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": [
"images",
"opening_hours_24_7",
{
"question": {
"en": "How much does it cost to use the cleaning service?",
@ -283,6 +284,24 @@
],
"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": {
"en": "Is this bicycle cleaning service automated?",
@ -297,8 +316,8 @@
{
"if": "automated=no",
"then": {
"en": "This is a manual bike washing station",
"nl": "Dit is een handmatig fietsschoonmaakpunt",
"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 - een persoon moet zelf de waterspuit richten naar de fiets",
"de": "Dies ist eine manuelle Fahrradwaschanlage",
"cs": "Jedná se o ruční mycí stanici kol",
"es": "Esta es una estación manual de lavado de bicicletas",
@ -309,8 +328,8 @@
{
"if": "automated=yes",
"then": {
"en": "This is an automated bike wash",
"nl": "Dit is een automatisch fietsschoonmaakpunt",
"en": "This is an automated bike wash. Your bicycle is placed in the device and everything happens automatically.",
"nl": "Dit is een automatisch fietsschoonmaakpunt - eens je fiets erin geplaats, wordt alles volledig automatisch proper gemaakt",
"de": "Dies ist eine automatische Fahrradwaschanlage",
"cs": "Jedná se o mytí kol bez obsluhy",
"es": "Esta es una estación automática de lavado de bicicletas",

View file

@ -413,10 +413,9 @@
"service:electricity",
"seating",
"dog-access",
"internet",
"internet-fee",
"internet-ssid",
"reviews"
"internet-all",
"reviews",
"toilet_at_amenity_lib.all"
],
"filter": [
"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",
"internet",
"internet-fee",
"internet-ssid"
"internet-ssid",
"toilet_at_amenity_lib.all"
],
"filter": [
"open_now",

View file

@ -73,8 +73,20 @@
"placeholder": {
"en": "Name of the mobility hub",
"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": {
@ -90,7 +102,8 @@
"nl": "Netwerk van deze mobiliteitshub"
},
"addExtraTags": [
"network:wikidata="
"network:wikidata=",
"nonetwork="
]
},
"render": {
@ -98,6 +111,16 @@
"nl": "Deze mobiliteitshub hoort bij het netwerk {network}"
},
"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",
"then": {
@ -107,7 +130,8 @@
"hideInAnswer": "_country!=nl",
"icon": "./assets/layers/mobility_hub/hub-gd.svg",
"addExtraTags": [
"network:wikidata=Q108742233"
"network:wikidata=Q108742233",
"nonetwork="
]
},
{
@ -119,7 +143,8 @@
"hideInAnswer": "_country!=be",
"icon": "./assets/layers/mobility_hub/logo-hoppin.svg",
"addExtraTags": [
"network:wikidata=Q124310711"
"network:wikidata=Q124310711",
"nonetwork="
]
},
{
@ -130,7 +155,8 @@
},
"hideInAnswer": "_country!=de",
"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"
}
}
]
],
"condition": "_geometry:type=Point"
}
],
"lineRendering": [

View file

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

View file

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

View file

@ -211,7 +211,14 @@
}
}
},
"level",
{
"builtin": "level",
"override": {
"labels+": [
"amenity-no-prefix"
]
}
},
{
"question": {
"en": "Are these toilets publicly accessible?",
@ -247,6 +254,7 @@
"mappings": [
{
"if": "access=yes",
"alsoShowIf": "access=public",
"then": {
"en": "Public access",
"de": "Der Zugang ist öffentlich",
@ -258,7 +266,38 @@
"da": "Offentlig adgang",
"ca": "Accés públic",
"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íč",
"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": [
"relevant-questions"
"relevant-questions",
"amenity-no-prefix"
],
"id": "toilet-access"
},
{
"id": "toilets-fee",
"labels": [
"relevant-questions"
"relevant-questions",
"amenity-no-prefix"
],
"condition": {
"and": [
@ -352,6 +377,8 @@
"da": "Er det gratis at benytte disse toiletter?",
"ca": "Aquest serveis són gratuïts?",
"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?"
},
"mappings": [
@ -366,7 +393,9 @@
"es": "Estos son baños de pago",
"da": "Det er betalingstoiletter",
"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"
},
@ -381,14 +410,17 @@
"da": "Gratis at bruge",
"ca": "Gratuït",
"cs": "Použití zdarma",
"es": "De uso gratuito"
"es": "De uso gratuito",
"pt": "Grátis para usar",
"sl": "Brezplačna uporaba"
}
}
]
},
{
"labels": [
"relevant-questions"
"relevant-questions",
"amenity-no-prefix"
],
"question": {
"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?",
"da": "Hvor meget skal man betale for disse toiletter?",
"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": {
"en": "The fee is {charge}",
@ -412,7 +445,8 @@
"es": "La tarifa es {charge}",
"da": "Gebyret er {charge}",
"ca": "La taxa és {charge}",
"cs": "Poplatek je {charge}"
"cs": "Poplatek je {charge}",
"sl": "Plačilo je {charge}"
},
"condition": {
"and": [
@ -442,7 +476,8 @@
]
},
"=labels": [
"relevant-questions"
"relevant-questions",
"amenity-no-prefix"
]
}
},
@ -454,6 +489,7 @@
"access!=no"
]
},
"#labels": "NOT included in amenity-no-prefix! The 'amenity' has their own opening hours",
"=labels": [
"relevant-questions",
"no-prefix"
@ -474,7 +510,8 @@
"id": "toilets-type",
"labels": [
"relevant-questions",
"prefixed"
"prefixed",
"amenity-prefixed"
],
"question": {
"en": "Which kind of toilets are these?",
@ -550,7 +587,8 @@
"id": "toilets-disposal",
"labels": [
"relevant-questions",
"prefixed"
"prefixed",
"amenity-prefixed"
],
"question": {
"en": "How is the waste handled?",
@ -589,7 +627,8 @@
"id": "gender_segregated",
"labels": [
"relevant-questions",
"no-prefix"
"no-prefix",
"amenity-no-prefix"
],
"question": {
"en": "Are these toilets gender-segregated?",
@ -636,7 +675,8 @@
"id": "menstrual_products",
"labels": [
"relevant-questions",
"prefixed"
"prefixed",
"amenity-prefixed"
],
"question": {
"en": "Are free, menstrual products distributed here?",
@ -699,7 +739,8 @@
"id": "menstrual_products_location",
"labels": [
"relevant-questions",
"prefixed"
"prefixed",
"amenity-prefixed"
],
"question": {
"en": "Where are the free menstrual products located?",
@ -769,9 +810,11 @@
},
{
"id": "toilets-changing-table",
"#labels": "Very weird case: we transfer this as is to the 'amenity'-layer",
"labels": [
"relevant-questions",
"no-prefix"
"no-prefix",
"amenity-prefixed"
],
"question": {
"en": "Is a changing table (to change diapers) available?",
@ -819,7 +862,8 @@
{
"labels": [
"relevant-questions",
"no-prefix"
"no-prefix",
"amenity-prefixed"
],
"question": {
"en": "Where is the changing table located?",
@ -917,7 +961,8 @@
"id": "toilet-supervised",
"labels": [
"relevant-questions",
"no-prefix"
"no-prefix",
"amenity-no-prefix"
],
"question": {
"en": "Is this toilets supervised by a person?",
@ -964,7 +1009,8 @@
{
"id": "toilet-has-paper",
"labels": [
"relevant-questions"
"relevant-questions",
"amenity-prefixed"
],
"question": {
"en": "Does one have to bring their own toilet paper to this toilet?",
@ -1014,7 +1060,8 @@
{
"labels": [
"relevant-questions",
"prefixed"
"prefixed",
"amenity-prefixed"
],
"id": "toilet-handwashing",
"question": {
@ -1060,7 +1107,8 @@
"id": "toilet-drying",
"labels": [
"relevant-questions",
"prefixed"
"prefixed",
"amenity-prefixed"
],
"question": {
"en": "Do these toilets have a device to dry your hands?",
@ -1110,12 +1158,23 @@
]
}
},
{
"builtin":
"description",
"override": {
"labels": [
"amenity-no-prefix",
"no-prefix",
"relevant-questions"
]
}
},
{
"id": "wheelchair-group",
"labels": [
"relevant-questions",
"prefixed"
"prefixed",
"amenity-prefixed"
],
"render": {
"special": {
@ -1131,7 +1190,8 @@
"relevant-questions",
"wheelchair",
"hidden",
"no-prefix"
"no-prefix",
"amenity-no-prefix"
],
"question": {
"en": "Is there a dedicated toilet for wheelchair users?",
@ -1142,7 +1202,8 @@
"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": "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": [
{
@ -1171,7 +1232,8 @@
"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"
"cs": "Žádný bezbariérový přístup",
"sl": "Ni dostopno invalidom na vozičku"
}
},
{
@ -1205,7 +1267,8 @@
"wheelchair",
"hidden",
"relevant-questions",
"prefixed"
"prefixed",
"amenity-prefixed"
],
"render": {
"special": {
@ -1231,7 +1294,8 @@
"wheelchair",
"hidden",
"relevant-questions",
"prefixed"
"prefixed",
"amenity-prefixed"
],
"render": {
"special": {
@ -1249,7 +1313,8 @@
"labels": [
"hidden",
"relevant-questions",
"prefixed"
"prefixed",
"amenity-prefixed"
],
"render": {
"en": "Wheelchair accessible toilet",
@ -1284,7 +1349,7 @@
]
},
{
"id": "wheelchair-access",
"id": "toilet-wheelchair-access",
"question": {
"en": "Is the wheelchair-accessible toilet locked?",
"nl": "Is de rolstoeltoegankelijke toilet op slot?"
@ -1311,7 +1376,8 @@
"hidden",
"wheelchair",
"relevant-questions",
"prefixed"
"prefixed",
"amenity-prefixed"
],
"mappings": [
{
@ -1429,7 +1495,8 @@
"labels": [
"wheelchair",
"hidden",
"relevant-questions"
"relevant-questions",
"amenity-prefixed"
],
"render": {
"special": {
@ -1439,17 +1506,21 @@
}
},
{
"id": "adult-changing-table-title",
"labels": [
"hidden",
"builtin": "adult_changing_table.title",
"override": {
"labels+": [
"hidden",
"prefixed",
"adult-changing-table"
"adult-changing-table",
"amenity-prefixed"
],
"render": {
"en": "<h3>Adult changing table</h3>",
"nl": "<h3>Verzorgingstafel voor volwassenen</h3>"
},
"condition": "changing_table:adult=yes"
"condition": {
"and": [
"changing_table:adult=yes"
]
},
"classes": "bold text-lg"
}
},
{
"id": "adult-changing-table",
@ -1457,7 +1528,8 @@
"prefixed",
"hidden",
"relevant-questions",
"adult-changing-table"
"adult-changing-table",
"amenity-prefixed"
],
"question": {
"en": "Does this toilet have an adult changing table?",
@ -1482,175 +1554,29 @@
]
},
{
"id": "adult-changing-table-height",
"labels": [
"hidden",
"prefixed",
"adult-changing-table"
],
"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": "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>"
}
"builtin": "adult_changing_table.relevant_questions",
"override": {
"labels+": [
"hidden",
"prefixed",
"adult-changing-table",
"amenity-prefixed"
],
"condition": {
"and+": [
"changing_table:adult=yes"
]
}
],
"freeform": {
"key": "changing_table:adult:height",
"type": "pfloat"
},
"render": {
"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"
}
}
]
"prefix": "changing_table:adult"
},
{
"id": "questions-adult-changing-table",
"labels": [
"hidden",
"relevant-questions",
"adult-changing-table"
"adult-changing-table",
"amenity-prefixed"
],
"render": {
"special": {
@ -1753,33 +1679,6 @@
"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": [],
"tagRenderings": [
"images",
"level",
{
"question": {
"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?"
},
"id": "images",
"render": {
"en": "Access is {toilets:access}",
"de": "Zugang ist {toilets:access}",
"fr": "L'accès est {toilets:access}",
"nl": "Toegankelijkheid is {toilets:access}",
"it": "L'accesso è {toilets:access}",
"es": "El acceso es {toilets:access}",
"da": "Adgang er {toilets:access}",
"ca": "L'accés és {toilets:access}",
"cs": "Přístup je {toilets:access}",
"sl": "Dostop je {toilets:access}",
"uk": "Доступ - {toilets:access}"
},
"freeform": {
"key": "toilets:access",
"addExtraTags": [
"fixme=the tag toilets:access was filled out by the user and might need refinement"
"special": {
"before": "{image_carousel(toilets:panoramax;toilets:mapillary;toilets:images)}",
"type": "image_upload",
"image_key": "toilets:panoramax",
"label": {
"en": "Add a picture of the toilets",
"nl": "Voeg een foto van de toiletten toe"
}
}
}
},
{
"builtin": "toilet.amenity-no-prefix",
"prefix": "toilets",
"override": {
"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",
"override": {
"condition": "toilets:access!=no",
"condition": {
"and+": [
"toilets:access!=no"
]
},
"question": {
"en": "When is the amenity where these toilets are located open?",
"de": "Wann ist der Ort, an dem sich diese Toiletten befinden, geöffnet?",
@ -348,91 +182,11 @@
}
},
{
"id": "toilets-wheelchair",
"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",
"builtin": "toilet.amenity-prefixed",
"override": {
"render": "{toilets:description}",
"freeform": {
"key": "toilets:description",
"type": "string"
}
"labels+": [
"relevant_questions"
]
}
}
],

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",
"waste_disposal"
"waste_disposal",
"excrement_bag_dispenser"
]
}

View file

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

View file

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

View file

@ -11986,9 +11986,6 @@
},
"3": {
"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?",
@ -12135,42 +12132,6 @@
"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": {
"mappings": {
"0": {

View file

@ -10653,9 +10653,6 @@
},
"3": {
"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é?",
@ -10795,42 +10792,6 @@
"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": {
"mappings": {
"0": {

View file

@ -2210,9 +2210,6 @@
},
"3": {
"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?",
@ -2336,35 +2333,6 @@
}
},
"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": {
"mappings": {
"0": {

View file

@ -11917,7 +11917,7 @@
"toilet-access": {
"mappings": {
"0": {
"then": "Der Zugang ist öffentlich"
"then": "Öffentlicher Zugang"
},
"1": {
"then": "Der Zugang ist nur für Kunden"
@ -11927,9 +11927,6 @@
},
"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?",
@ -12076,42 +12073,6 @@
"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": {
"mappings": {
"0": {

View file

@ -42,6 +42,63 @@
"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": {
"description": "We will complete data from advertising features with reference, operator and lit",
"name": "Advertisement",
@ -1568,10 +1625,10 @@
"automated": {
"mappings": {
"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": {
"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?"
@ -1659,6 +1716,9 @@
"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"
},
"11": {
"then": "An anchor - a metal loop wide enough for a bike lock attached to a wall, the floor or a boulder."
},
"2": {
"then": "Wheelbenders / rack"
},
@ -7423,10 +7483,21 @@
"description": "Shows the allowed speed for every road",
"name": "Maxspeed",
"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": {
"mappings": {
"0": {
"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?",
@ -12260,54 +12331,6 @@
},
"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": {
"override": {
"question": "What is the email address one can send to in case of troubles or questions?"
@ -12378,9 +12401,6 @@
},
"3": {
"then": "Accessible, but one has to ask a key to enter"
},
"4": {
"then": "Public access"
}
},
"question": "Are these toilets publicly accessible?",
@ -12614,42 +12634,6 @@
"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": {
"mappings": {
"0": {

View file

@ -10852,9 +10852,6 @@
},
"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?",
@ -10994,42 +10991,6 @@
"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": {
"mappings": {
"0": {

View file

@ -6609,7 +6609,7 @@
"toilet-access": {
"mappings": {
"0": {
"then": "Accès public"
"then": "Accès publique"
},
"1": {
"then": "Accès réservé aux clients"
@ -6619,9 +6619,6 @@
},
"3": {
"then": "Accessible, mais vous devez demander la clé"
},
"4": {
"then": "Accès publique"
}
},
"question": "Ces toilettes sont-elles accessibles au public ?",
@ -6748,42 +6745,6 @@
}
},
"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": {
"mappings": {
"0": {

View file

@ -2898,9 +2898,6 @@
},
"3": {
"then": "Accessibile, ma occorre chiedere una chiave per accedere"
},
"4": {
"then": "Accesso pubblico"
}
},
"question": "Questi servizi igienici sono aperti al pubblico?",
@ -2985,35 +2982,6 @@
},
"toilet_at_amenity": {
"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": {
"mappings": {
"0": {

View file

@ -41,6 +41,62 @@
"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": {
"description": "We vullen de informatie over de advertentie aan met de referentie, de operator en de verlichting",
"name": "Reclame",
@ -1520,10 +1576,10 @@
"automated": {
"mappings": {
"0": {
"then": "Dit is een handmatig fietsschoonmaakpunt"
"then": "Dit is een handmatig fietsschoonmaakpunt - een persoon moet zelf de waterspuit richten naar de fiets"
},
"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?"
@ -1611,6 +1667,9 @@
"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"
},
"11": {
"then": "Een anker - een metalen lus waar een fietsslot door kan en vastgemaakt aan de muur of vloer"
},
"2": {
"then": "Wielrek/lussen"
},
@ -9847,54 +9906,6 @@
},
"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": {
"override": {
"question": "Naar welk email address kan men sturen voor vragen of om problemen te melden?"
@ -9965,9 +9976,6 @@
},
"3": {
"then": "Toegankelijk na het vragen van de sleutel"
},
"4": {
"then": "Publiek toegankelijk"
}
},
"question": "Zijn deze toiletten publiek toegankelijk?",
@ -10196,42 +10204,6 @@
},
"name": "Toilet in een voorziening",
"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": {
"mappings": {
"0": {

View file

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

View file

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

View file

@ -443,33 +443,17 @@
"description": "Stranišča z vsaj enim invalidom na vozičku dostopnim straniščem"
}
},
"title": {
"render": "Stranišče"
}
},
"toilet_at_amenity": {
"tagRenderings": {
"toilet-access": {
"mappings": {
"0": {
"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": {
"question": "Koliko je potrebno plačati za ta stranišča?",
"render": "Plačilo je {toilets:charge}"
"render": "Plačilo je {charge}"
},
"toilets-fee": {
"mappings": {
@ -481,7 +465,14 @@
}
},
"question": "Ali so ta stranišča brezplačna za uporabo?"
},
}
},
"title": {
"render": "Stranišče"
}
},
"toilet_at_amenity": {
"tagRenderings": {
"toilets-wheelchair": {
"mappings": {
"1": {

View file

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

View file

@ -98,16 +98,15 @@
"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: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",
"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: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",
"clean:licenses": "find . -type f -name \"*.license\" -exec rm -f {} +",
"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",
"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:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -",
"clean:tests": "find . -type f -name \"*.doctest.ts\" | xargs -r rm",
@ -224,8 +223,7 @@
"latlon2country": "^1.2.7",
"libphonenumber-js": "^1.11.19",
"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",
"monaco-editor": "^0.46.0",
"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-x-2 {
border-left-width: 2px;
border-right-width: 2px;
}
.border-y {
border-top-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);
}
.\[a-zA-Z0-9\:_-\] {
a-z-a--z0-9: -;
}
.\[a-zA-Z0-9\:_\] {
a-z-a--z0-9: ;
}
@ -5286,6 +5295,11 @@ input[type="text"] {
border-radius: 0.5rem;
}
.border-low-interaction {
border-color: var(--interaction-border);
border-style: dashed;
}
.border-region {
border: 2px dashed var(--interactive-background);
border-radius: 0.5rem;
@ -5465,6 +5479,10 @@ textarea {
h2.group {
/* For flowbite accordions */
margin: 0;
top: 0;
position: -webkit-sticky;
position: sticky;
z-index: 12;
}
.group button {

View file

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

View file

@ -9,16 +9,12 @@ import {
DoesImageExist,
PrevalidateTheme,
ValidateLayer,
ValidateThemeEnsemble,
ValidateThemeEnsemble
} from "../src/Models/ThemeConfig/Conversion/Validation"
import { Translation } from "../src/UI/i18n/Translation"
import { PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer"
import { PrepareTheme } from "../src/Models/ThemeConfig/Conversion/PrepareTheme"
import {
Conversion,
DesugaringContext,
DesugaringStep,
} from "../src/Models/ThemeConfig/Conversion/Conversion"
import { Conversion, DesugaringContext, DesugaringStep } from "../src/Models/ThemeConfig/Conversion/Conversion"
import { Utils } from "../src/Utils"
import Script from "./Script"
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 { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages"
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.
// 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 {
public static readonly layerPath = "./src/assets/generated/layers/"
public static readonly layerPath = "./public/assets/generated/layers/"
public static readonly themePath = "./public/assets/generated/themes/"
constructor() {
@ -190,7 +350,7 @@ class LayerOverviewUtils extends Script {
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)) {
return true
}
@ -202,7 +362,6 @@ class LayerOverviewUtils extends Script {
for (const path of sourcefile) {
const hasChange = statSync(path).mtime > targetModified
if (hasChange) {
console.log("File ", targetfile, " should be updated as ", path, "has been changed")
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(
trs: QuestionableTagRenderingConfigJson[]
): Map<string, QuestionableTagRenderingConfigJson> {
@ -481,13 +629,6 @@ class LayerOverviewUtils extends Script {
?.split(",") ?? []
)
const layerWhitelist = new Set(
args
.find((a) => a.startsWith("--layers="))
?.substring("--layers=".length)
?.split(",") ?? []
)
const forceReload = args.some((a) => a == "--force")
const licensePaths = new Set<string>()
@ -495,7 +636,7 @@ class LayerOverviewUtils extends Script {
licensePaths.add(licenses[i].path)
}
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)
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(
@ -606,81 +744,101 @@ class LayerOverviewUtils extends Script {
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(
doesImageExist: DoesImageExist,
forceReload: boolean,
whitelist: Set<string>
doesImageExist: DoesImageExist
): Map<string, LayerConfigJson> {
// 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.
console.log("------------- VALIDATING THE BUILTIN QUESTIONS ---------------")
const sharedTagRenderings = this.getSharedTagRenderings(doesImageExist)
console.log(" ---------- VALIDATING BUILTIN LAYERS ---------")
const state: DesugaringContext = {
tagRenderings: LayerOverviewUtils.asDict(sharedTagRenderings),
tagRenderingOrder: sharedTagRenderings.map((tr) => tr.id),
sharedLayers: AllSharedLayers.getSharedLayersConfigs(),
}
const sharedLayers = new Map<string, LayerConfigJson>()
const prepLayer = new PrepareLayer(state)
const skippedLayers: string[] = []
const recompiledLayers: string[] = []
let warningCount = 0
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
}
const sharedQuestions = this.getSharedTagRenderings(doesImageExist)
const allLayerConfigs = this.getAllLayerConfigs()
const sharedQuestionsDef = allLayerConfigs.find(l => l.id === "questions")
sharedQuestionsDef.tagRenderings = sharedQuestions
const dependencyGraph = LayerConfigDependencyGraph.buildDirectDependencies(allLayerConfigs)
const levels = LayerConfigDependencyGraph.buildLevels(dependencyGraph)
const layerState = new Map<string, "clean" | "dirty" | "changed">()
console.log("# BUILD PLAN\n\n")
for (const levelInfo of levels) {
if (levelInfo.loop) {
console.log(`(LOOP)`)
}
{
const targetPath =
LayerOverviewUtils.layerPath +
sharedLayerPath.substring(sharedLayerPath.lastIndexOf("/"))
if (!forceReload && !this.shouldBeUpdated(sharedLayerPath, targetPath)) {
try {
const sharedLayer = JSON.parse(readFileSync(targetPath, "utf8"))
sharedLayers.set(sharedLayer.id, sharedLayer)
skippedLayers.push(sharedLayer.id)
continue
} catch (e) {
throw "Could not parse " + targetPath + " : " + e
let allClean = true
for (const id of levelInfo.ids) {
const deps = dependencyGraph.get(id) ?? []
const dirtyDeps = deps.filter(dep => {
const depState = layerState.get(dep)
if (levelInfo.loop && depState === undefined) {
const depIsClean =
LayerOverviewUtils.shouldBeUpdated(
LayerBuilder.sourcePath(dep),
LayerBuilder.targetPath(dep))
if (depIsClean) {
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(", ")})`)
}
}
const parsed = this.parseLayer(doesImageExist, prepLayer, sharedLayerPath)
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 (allClean) {
console.log("\n")
}
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 " +
recompiledLayers.join(", ") +
" and skipped " +
skippedLayers.length +
" layers. Detected " +
warningCount +
" warnings"
)
// We always need the calculated tags of 'usersettings', so we export them separately
this.extractJavascriptCodeForLayer(
state.sharedLayers.get("usersettings"),
const builder = new LayerBuilder(allLayerConfigs, dependencyGraph, levels, layerState, sharedQuestions)
builder.writeLayer(sharedQuestionsDef)
const allLayers = builder.convertStrict({}, ConversionContext.construct([], []))
if (layerState.get("usersettings") !== "clean") {
// We always need the calculated tags of 'usersettings', so we export them separately if dirty
LayerOverviewUtils.extractJavascriptCodeForLayer(
allLayers.get("usersettings"),
"./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"))
}
private extractJavascriptCodeForLayer(l: LayerConfigJson, targetPath?: string) {
public static extractJavascriptCodeForLayer(l: LayerConfigJson, targetPath?: string) {
if (!l) {
return // Probably a bootstrapping run
}
@ -858,7 +1016,7 @@ class LayerOverviewUtils extends Script {
LayerOverviewUtils.extractLayerIdsFrom(themeFile, false)
).map((id) => LayerOverviewUtils.layerPath + id + ".json")
if (!forceReload && !this.shouldBeUpdated([themePath, ...usedLayers], targetPath)) {
if (!forceReload && !LayerOverviewUtils.shouldBeUpdated([themePath, ...usedLayers], targetPath)) {
fixed.set(
themeFile.id,
JSON.parse(

View file

@ -5,12 +5,12 @@
mkdir -p ./src/assets/generated/layers
mkdir -p ./public/assets/generated/themes
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
cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json
echo '{}' > ./src/assets/generated/layers/favourite.json
echo '{}' > ./src/assets/generated/layers/summary.json
echo '{}' > ./src/assets/generated/layers/last_click.json
echo '{}' > ./src/assets/generated/layers/search.json
echo '[]' > ./src/assets/generated/theme_overview.json
echo '{}' > ./src/assets/generated/layers/geocoded_image.json
echo '{}' > ./public/assets/generated/layers/favourite.json
echo '{}' > ./public/assets/generated/layers/summary.json
echo '{}' > ./public/assets/generated/layers/last_click.json
echo '{}' > ./public/assets/generated/layers/search.json
echo '[]' > ./public/assets/generated/theme_overview.json
echo '{}' > ./public/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 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 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
if [ $? -ne 0 ]; then

View file

@ -1,5 +1,5 @@
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 { AllSharedLayers } from "./AllSharedLayers"
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 TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
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 { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"

View file

@ -2,7 +2,7 @@ import { BBox } from "../BBox"
import { Feature, Geometry } from "geojson"
import { DefaultPinIcon } from "../../Models/Constants"
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 { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { GeoOperations } from "../GeoOperations"

View file

@ -10,7 +10,7 @@ import translators from "../../assets/translators.json"
import codeContributors from "../../assets/contributors.json"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
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 LinkToWeblate from "../../UI/Base/LinkToWeblate"
import FeatureSwitchState from "./FeatureSwitchState"

View file

@ -1,42 +1,14 @@
import { Utils } from "../../Utils"
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
export class ThemeMetaTagging {
public static readonly themeName = "usersettings"
public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
feat.properties._description
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
?.at(1)
)
Utils.AddLazyProperty(
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"
}
}
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
Utils.AddLazyProperty(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])
if (layer === undefined) {
if (!layer) {
context.err("Layer '" + split[0] + "' not found")
}
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
)
if (expandedFilter === undefined) {

View file

@ -6,6 +6,8 @@ import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRende
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { Utils } from "../../../Utils"
import { AddContextToTranslations } from "./AddContextToTranslations"
import AddPrefixToTagRenderingConfig from "./AddPrefixToTagRenderingConfig"
import { Translatable } from "../Json/Translatable"
export class ExpandTagRendering extends Conversion<
| string
@ -208,6 +210,21 @@ export class ExpandTagRendering extends Conversion<
let matchingTrs: (TagRenderingConfigJson & { id: string })[]
if (id === "*") {
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("*")) {
const id_ = id.substring(1)
matchingTrs = layerTrs.filter((tr) => tr["labels"]?.indexOf(id_) >= 0)
@ -249,6 +266,25 @@ export class ExpandTagRendering extends Conversion<
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(
tr: string | { builtin: string | string[] } | TagRenderingConfigJson,
ctx: ConversionContext
@ -310,6 +346,7 @@ export class ExpandTagRendering extends Conversion<
if (
key === "builtin" ||
key === "override" ||
key === "prefix" ||
key === "id" ||
key.startsWith("#")
) {
@ -343,15 +380,10 @@ export class ExpandTagRendering extends Conversion<
Utils.NoNull(Array.from(state.sharedLayers.keys())),
(s) => s
)
if (state.sharedLayers.size === 0) {
ctx.warn(
"BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " +
name +
": layer " +
layerName +
" not found for now, but ignoring as this is a bootstrapping run. "
)
if (candidates.length === 0) {
ctx.err("While reusing a tagRendering: " + name + "; no candidates in layer " + layerName)
} else {
console.error("Bench was not found...")
ctx.err(
": While reusing tagrendering: " +
name +
@ -363,22 +395,29 @@ export class ExpandTagRendering extends Conversion<
}
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(
(id) => layerName + "." + id
)
}
candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i)
ctx.err(
"The tagRendering with identifier " +
name +
" was not found.\n\tDid you mean one of " +
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
}
for (let foundTr of lookup) {
foundTr = Utils.Clone(foundTr)
foundTr = ExpandTagRendering.applyKeyPrefix(tr["prefix"], foundTr, ctx)
ctx.MergeObjectsForOverride(tr["override"] ?? {}, foundTr)
if (names.length == 1) {
foundTr["id"] = tr["id"] ?? foundTr["id"]

View file

@ -1,18 +1,6 @@
import {
Concat,
DesugaringContext,
DesugaringStep,
Each,
FirstOf,
Fuse,
On,
SetDefault,
} from "./Conversion"
import { Concat, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault } from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import {
MinimalTagRenderingConfigJson,
TagRenderingConfigJson,
} from "../Json/TagRenderingConfigJson"
import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
@ -210,7 +198,7 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
if (noLabels.length > 1) {
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) {
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)
}
}

View file

@ -1,14 +1,4 @@
import {
Concat,
Conversion,
DesugaringContext,
DesugaringStep,
Each,
Fuse,
On,
Pass,
SetDefault,
} from "./Conversion"
import { Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault } from "./Conversion"
import { ThemeConfigJson } from "../Json/ThemeConfigJson"
import { PrepareLayer, RewriteSpecial } from "./PrepareLayer"
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))
if (unused.length > 0) {
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: " +
unused.join(", ") +
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore"
`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(", ")}
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

View file

@ -78,6 +78,7 @@ export interface LayerConfigJson {
| undefined
| "special"
| "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?
@ -422,8 +423,15 @@ export interface LayerConfigJson {
| string
| {
id?: string
/**
* Special value: "<layerid>.title" will return the layer's title for an element
*/
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
| (RewritableConfigJson<

View file

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

View file

@ -225,7 +225,9 @@ export interface TagRenderingConfigJson {
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

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

View file

@ -26,7 +26,7 @@
import SelectedElementView from "./SelectedElementView.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
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 ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray"
import DownloadPanel from "../DownloadFlow/DownloadPanel.svelte"

View file

@ -3,7 +3,7 @@
export let expanded = 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
if (noBorder) {
defaultClass = "unstyled w-full flex-grow"
@ -11,7 +11,8 @@
</script>
<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"}>
<slot name="header" />
</span>

View file

@ -19,7 +19,7 @@
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import ShowDataLayer from "../Map/ShowDataLayer"
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 { onDestroy } from "svelte"
import { BBox } from "../../Logic/BBox"

View file

@ -160,7 +160,7 @@ export class OH {
}
for (let i = newList.length - 1; i >= 0 && doAddEntry; i--) {
let guard = newList[i]
const guard = newList[i]
if (maybeAdd.weekday != guard.weekday) {
// Not the same day
continue
@ -236,9 +236,8 @@ export class OH {
/**
* Gives the number of hours since the start of day.
* E.g.
* startTime({startHour: 9, startMinuts: 15}) == 9.25
* @param oh
*
* // OH.startTime({startHour: 9, startMinutes: 15}) // => 9.25
*/
public static startTime(oh: OpeningHour): number {
return oh.startHour + oh.startMinutes / 60
@ -346,8 +345,8 @@ export class OH {
const split = rule.trim().replace(/, */g, ",").split(" ")
if (split.length == 1) {
// First, try to parse this rule as a rule without weekdays
let timeranges = OH.ParseHhmmRanges(rule)
let weekdays = [0, 1, 2, 3, 4, 5, 6]
const timeranges = OH.ParseHhmmRanges(rule)
const weekdays = [0, 1, 2, 3, 4, 5, 6]
return OH.multiply(weekdays, timeranges)
}
@ -450,7 +449,7 @@ export class OH {
return ohs
}
/*
/**
This function converts a number of ranges (generated by OpeningHours.js) into all the hours of day that a change occurs.
E.g.
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
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
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(
ranges: {
@ -483,8 +487,7 @@ This list will be sorted
startOfDay.setHours(0, 0, 0, 0)
// The number of seconds since the start of the day
// @ts-ignore
const changeMoment: number = (range.startDate - startOfDay) / 1000
const changeMoment: number = (range.startDate.getTime() - startOfDay.getTime()) / 1000
if (changeHours.indexOf(changeMoment) < 0) {
changeHours.push(changeMoment)
changeHourText.push(
@ -493,8 +496,7 @@ This list will be sorted
}
// The number of seconds till between the start of the day and closing
// @ts-ignore
let changeMomentEnd: number = (range.endDate - startOfDay) / 1000
const changeMomentEnd: number = (range.endDate.getTime() - startOfDay.getTime()) / 1000
if (changeMomentEnd >= 24 * 60 * 60) {
if (extrachangeHours.indexOf(changeMomentEnd) < 0) {
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[][]
startingMonday: Date
} {
const today = new Date()
today ??= new Date()
today.setHours(0, 0, 0, 0)
const lastMonday = OH.getMondayBefore(today)
const nextSunday = new Date(lastMonday)
@ -699,7 +706,7 @@ This list will be sorted
public static weekdaysIdentical(openingRanges: OpeningRange[][], startday = 0, endday = 4) {
const monday = openingRanges[startday]
for (let i = startday + 1; i <= endday; i++) {
let weekday = openingRanges[i]
const weekday = openingRanges[i]
if (weekday.length !== monday.length) {
return false
}
@ -831,12 +838,12 @@ This list will be sorted
}
return [parsed]
} else if (split.length == 2) {
let start = OH.ParseWeekday(split[0])
let end = OH.ParseWeekday(split[1])
const start = OH.ParseWeekday(split[0])
const end = OH.ParseWeekday(split[1])
if ((start ?? null) === null || (end ?? null) === null) {
return null
}
let range = []
const range = []
for (let i = start; i <= end; i++) {
range.push(i)
}
@ -847,8 +854,8 @@ This list will be sorted
}
private static ParseWeekdayRanges(weekdays: string): number[] {
let ranges = []
let split = weekdays.split(",")
const ranges = []
const split = weekdays.split(",")
for (const weekday of split) {
const parsed = OH.ParseWeekdayRange(weekday)
if (parsed === undefined || parsed === null) {
@ -1054,7 +1061,7 @@ export class ToTextualDescription {
languages[supportedLanguage] = "{a}. {b}"
}
}
let chainer = new TypedTranslation<{ a; b }>(languages)
const chainer = new TypedTranslation<{ a; b }>(languages)
let tr = trs[0]
for (let i = 1; i < trs.length; 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 { VariableUiElement } from "../Base/VariableUIElement"
import Table from "../Base/Table"
import { Translation } from "../i18n/Translation"
import { Translation, TypedTranslation } from "../i18n/Translation"
import Loading from "../Base/Loading"
import opening_hours from "opening_hours"
import Locale from "../i18n/Locale"
@ -73,7 +73,7 @@ export default class OpeningHoursVisualization extends Toggle {
ranges: OpeningRange[][],
lastMonday: Date
): 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)) {
// The normal case: we have items for the coming days
return OpeningHoursVisualization.ConstructVizTable(oh, ranges, lastMonday)
@ -98,8 +98,7 @@ export default class OpeningHoursVisualization extends Toggle {
const today = new Date()
today.setHours(0, 0, 0, 0)
// @ts-ignore
const todayIndex = Math.ceil((today - rangeStart) / (1000 * 60 * 60 * 24))
const todayIndex = Math.ceil((today.getTime() - rangeStart.getTime()) / (1000 * 60 * 60 * 24))
// 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
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)
startOfDay.setHours(0, 0, 0, 0)
// @ts-ignore
const startpoint = (range.startDate - startOfDay) / 1000 - earliestOpen
const startpoint = (range.startDate.getTime() - startOfDay.getTime()) / 1000 - earliestOpen
// prettier-ignore
// @ts-ignore
const width = (100 * (range.endDate - range.startDate) / 1000) / (latestclose - earliestOpen);
const width = (100 * (range.endDate.getTime() - range.startDate.getTime()) / 1000) / (latestclose - earliestOpen)
const startPercentage = (100 * startpoint) / availableArea
return new FixedUiElement(textToShow)
.SetStyle(`left:${startPercentage}%; width:${width}%`)
@ -236,7 +233,7 @@ export default class OpeningHoursVisualization extends Toggle {
changeHourText: string[],
earliestOpen: number
): [BaseUIElement, string] {
let header: BaseUIElement[] = []
const header: BaseUIElement[] = []
header.push(
...OpeningHoursVisualization.CreateLinesAtChangeHours(
@ -249,7 +246,7 @@ export default class OpeningHoursVisualization extends Toggle {
let showHigher = false
let showHigherUsed = false
for (let i = 0; i < changeHours.length; i++) {
let changeMoment = changeHours[i]
const changeMoment = changeHours[i]
const offset = (100 * (changeMoment - earliestOpen)) / availableArea
if (offset < 0 || offset > 100) {
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, ...
* */
private static ShowSpecialCase(oh: any) {
const opensAtDate = oh.getNextChange()
if (opensAtDate === undefined) {
const comm = oh.getComment() ?? oh.getUnknown()
if (!!comm) {
return new FixedUiElement(comm)
}
if (oh.getState()) {
return Translations.t.general.opening_hours.open_24_7.Clone()
}
return Translations.t.general.opening_hours.closed_permanently.Clone()
private static ShowSpecialCase(oh: opening_hours) {
const nextChange = oh.getNextChange()
if (nextChange !== undefined) {
const nowOpen = oh.getState(new Date())
const t = Translations.t.general.opening_hours
const tr: TypedTranslation<{ date }> = nowOpen ? t.open_until : t.closed_until
const date = nextChange.toLocaleString()
return tr.Subs({ date })
}
return Translations.t.general.opening_hours.closed_until.Subs({
date: opensAtDate.toLocaleString(),
})
const comment = oh.getComment() ?? oh.getUnknown()
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 tags: UIEventSource<OsmTags>
export let labels: string[]
export let blacklist: string[]
export let header: string
export let layer: LayerConfig
@ -22,11 +23,15 @@
}
let tagRenderings: TagRenderingConfig[] = []
let seenIds = new Set<string>()
let blacklistSet = new Set(blacklist)
for (const label of labels) {
for (const tr of layer.tagRenderings) {
if (seenIds.has(tr.id)) {
continue
}
if (blacklistSet.has(tr.id) || tr.labels.some(l => blacklistSet.has(l))) {
continue
}
if (label === tr.id || tr.labels.some((l) => l === label)) {
tagRenderings.push(tr)
seenIds.add(tr.id)

View file

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

View file

@ -14,7 +14,7 @@ import NearbyImagesCollapsed from "../Image/NearbyImagesCollapsed.svelte"
class NearbyImageVis implements SpecialVisualizationSvelte {
// 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",
defaultValue: "closed",
@ -26,7 +26,7 @@ class NearbyImageVis implements SpecialVisualizationSvelte {
doc: "If 'readonly' or 'yes', will not show the 'link'-button",
},
]
group: "images"
group = "images"
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"
funcName = "nearby_images"
@ -65,6 +65,7 @@ export class ImageVisualisations {
args: [
{
name: "image_key",
type: "key",
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 ",
},
@ -95,6 +96,7 @@ export class ImageVisualisations {
needsUrls: [Imgur.apiUrl, ...Imgur.supportingUrls],
args: [
{
type: "key",
name: "image_key",
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
defaultValue: "panoramax",

View file

@ -186,6 +186,11 @@ export default class TagrenderingManipulationSpecialVisualisations {
name: "labels",
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(
state: SpecialVisualizationState,
@ -194,8 +199,9 @@ export default class TagrenderingManipulationSpecialVisualisations {
selectedElement: Feature,
layer: LayerConfig
): SvelteUIElement {
const [header, labelsStr] = argument
const [header, labelsStr, blacklistStr] = argument
const labels = labelsStr.split(";").map((x) => x.trim())
const blacklist = blacklistStr?.split(";")?.map(x => x.trim()) ?? []
return new SvelteUIElement(GroupedView, {
state,
tags,
@ -203,6 +209,7 @@ export default class TagrenderingManipulationSpecialVisualisations {
layer,
header,
labels,
blacklist
})
},
},

View file

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

View file

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

View file

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