diff --git a/assets/layers/adult_changing_table/adult_changing_table.json b/assets/layers/adult_changing_table/adult_changing_table.json new file mode 100644 index 000000000..18ca9659e --- /dev/null +++ b/assets/layers/adult_changing_table/adult_changing_table.json @@ -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 adjustable in height", + "nl": "De verzorgingstafel is in hoogte verstelbaar" + } + } + ], + "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 met de hand aangepast", + "en": "The height of the adult changing table is adjusted manually" + } + }, + { + "if": "height:mechanism=electric", + "then": { + "nl": "De verzorgingstafel wordt door een electrische motor in hoogte versteld", + "en": "The height of the adult changing table is adjusted electrically" + } + } + ], + "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 with wheels and can be moved", + "nl": "De verschoningstafel voor volwassenen staat op tafelpoten met wielen 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" + ] + } + } + ] +} diff --git a/assets/layers/bike_cleaning/bike_cleaning.json b/assets/layers/bike_cleaning/bike_cleaning.json index 2c0cba8fd..4b459795a 100644 --- a/assets/layers/bike_cleaning/bike_cleaning.json +++ b/assets/layers/bike_cleaning/bike_cleaning.json @@ -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", diff --git a/assets/layers/cafe_pub/cafe_pub.json b/assets/layers/cafe_pub/cafe_pub.json index daa55af8f..5aab7dad0 100644 --- a/assets/layers/cafe_pub/cafe_pub.json +++ b/assets/layers/cafe_pub/cafe_pub.json @@ -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", diff --git a/assets/layers/excrement_bag_dispenser/excrement_bag_dispenser.json b/assets/layers/excrement_bag_dispenser/excrement_bag_dispenser.json new file mode 100644 index 000000000..75ceaf117 --- /dev/null +++ b/assets/layers/excrement_bag_dispenser/excrement_bag_dispenser.json @@ -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 + } +} \ No newline at end of file diff --git a/assets/layers/excrement_bag_dispenser/excrement_bags.svg b/assets/layers/excrement_bag_dispenser/excrement_bags.svg new file mode 100644 index 000000000..755f008ac --- /dev/null +++ b/assets/layers/excrement_bag_dispenser/excrement_bags.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/layers/excrement_bag_dispenser/excrement_bags.svg.license b/assets/layers/excrement_bag_dispenser/excrement_bags.svg.license new file mode 100644 index 000000000..a342db9b9 --- /dev/null +++ b/assets/layers/excrement_bag_dispenser/excrement_bags.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Jérémy Ragusa +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/layers/excrement_bag_dispenser/license_info.json b/assets/layers/excrement_bag_dispenser/license_info.json new file mode 100644 index 000000000..b15d15270 --- /dev/null +++ b/assets/layers/excrement_bag_dispenser/license_info.json @@ -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" + ] + } +] \ No newline at end of file diff --git a/assets/layers/food/food.json b/assets/layers/food/food.json index c259f6edb..3d02e4d0e 100644 --- a/assets/layers/food/food.json +++ b/assets/layers/food/food.json @@ -1433,7 +1433,8 @@ "dog-access", "internet", "internet-fee", - "internet-ssid" + "internet-ssid", + "toilet_at_amenity_lib.all" ], "filter": [ "open_now", diff --git a/assets/layers/mobility_hub/mobility_hub.json b/assets/layers/mobility_hub/mobility_hub.json index d3d0cc710..d55ae9937 100644 --- a/assets/layers/mobility_hub/mobility_hub.json +++ b/assets/layers/mobility_hub/mobility_hub.json @@ -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": [ diff --git a/assets/layers/questions/questions.json b/assets/layers/questions/questions.json index c12842a1a..f620399ba 100644 --- a/assets/layers/questions/questions.json +++ b/assets/layers/questions/questions.json @@ -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, diff --git a/assets/layers/shops/shops.json b/assets/layers/shops/shops.json index 9c6bf084b..9b9c70351 100644 --- a/assets/layers/shops/shops.json +++ b/assets/layers/shops/shops.json @@ -1326,7 +1326,8 @@ } }, "dog-access", - "description" + "description", + "toilet_at_amenity_lib.all" ], "filter": [ { diff --git a/assets/layers/toilet/toilet.json b/assets/layers/toilet/toilet.json index 65d3271b0..861637781 100644 --- a/assets/layers/toilet/toilet.json +++ b/assets/layers/toilet/toilet.json @@ -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": "

Adult changing table

", - "nl": "

Verzorgingstafel voor volwassenen

" - }, - "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 adjustable in height", - "nl": "De verzorgingstafel is in hoogte verstelbaar" - } + "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 met de hand aangepast", - "en": "The height of the adult changing table is adjusted manually" - } - }, - { - "if": "changing_table:adult:height:mechanism=electric", - "then": { - "nl": "De verzorgingstafel wordt door een electrische motor in hoogte versteld", - "en": "The height of the adult changing table is adjusted electrically" - } - } - ], - "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 with wheels and can be moved", - "nl": "De verschoningstafel voor volwassenen staat op tafelpoten met wielen 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" - ] - } } ] } diff --git a/assets/layers/toilet_at_amenity/toilet_at_amenity.json b/assets/layers/toilet_at_amenity/toilet_at_amenity.json index 3656a0eac..02b329cf6 100644 --- a/assets/layers/toilet_at_amenity/toilet_at_amenity.json +++ b/assets/layers/toilet_at_amenity/toilet_at_amenity.json @@ -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" + ] } } ], diff --git a/assets/layers/toilet_at_amenity_lib/toilet_at_amenity_lib.json b/assets/layers/toilet_at_amenity_lib/toilet_at_amenity_lib.json new file mode 100644 index 000000000..f084377f3 --- /dev/null +++ b/assets/layers/toilet_at_amenity_lib/toilet_at_amenity_lib.json @@ -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" + } + ] + } + ] +} diff --git a/assets/themes/waste/waste.json b/assets/themes/waste/waste.json index 5289104ba..f8cad0f8e 100644 --- a/assets/themes/waste/waste.json +++ b/assets/themes/waste/waste.json @@ -46,6 +46,7 @@ } }, "recycling", - "waste_disposal" + "waste_disposal", + "excrement_bag_dispenser" ] } \ No newline at end of file diff --git a/assets/themes/waste_basket/waste_basket.json b/assets/themes/waste_basket/waste_basket.json index dd19d1026..39b466d21 100644 --- a/assets/themes/waste_basket/waste_basket.json +++ b/assets/themes/waste_basket/waste_basket.json @@ -60,7 +60,8 @@ "override": { "minzoom": 12 } - } + }, + "excrement_bag_dispenser" ], "widenFactor": 2 } \ No newline at end of file diff --git a/langs/en.json b/langs/en.json index 456326f91..3e55ff894 100644 --- a/langs/en.json +++ b/langs/en.json @@ -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": " ", diff --git a/langs/layers/ca.json b/langs/layers/ca.json index bc3caf872..7defe1bff 100644 --- a/langs/layers/ca.json +++ b/langs/layers/ca.json @@ -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": { diff --git a/langs/layers/cs.json b/langs/layers/cs.json index a20424f67..669f78d88 100644 --- a/langs/layers/cs.json +++ b/langs/layers/cs.json @@ -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": { diff --git a/langs/layers/da.json b/langs/layers/da.json index ff92eddad..d451bfd80 100644 --- a/langs/layers/da.json +++ b/langs/layers/da.json @@ -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": { diff --git a/langs/layers/de.json b/langs/layers/de.json index b352a7b81..80e1586b6 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -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": { diff --git a/langs/layers/en.json b/langs/layers/en.json index 7503f57a2..c52493b32 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -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 manually" + }, + "1": { + "then": "The height of the adult changing table is adjusted electrically" + } + }, + "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 with wheels and can be moved" + } + }, + "question": "How is the adult changing table supported?" + }, + "height": { + "mappings": { + "0": { + "then": "The changing table is adjustable in height" + } + }, + "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 adjustable in height" - } - }, - "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 manually" - }, - "1": { - "then": "The height of the adult changing table is adjusted electrically" - } - }, - "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 with wheels and can be moved" - } - }, - "question": "How is the adult changing table supported?" - }, - "adult-changing-table-title": { - "render": "

Adult changing table

" - }, "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": { diff --git a/langs/layers/es.json b/langs/layers/es.json index c2f8bf4b6..6ae6f964a 100644 --- a/langs/layers/es.json +++ b/langs/layers/es.json @@ -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": { diff --git a/langs/layers/fr.json b/langs/layers/fr.json index fae2dae4c..3373c02e2 100644 --- a/langs/layers/fr.json +++ b/langs/layers/fr.json @@ -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": { diff --git a/langs/layers/it.json b/langs/layers/it.json index 182774792..126932447 100644 --- a/langs/layers/it.json +++ b/langs/layers/it.json @@ -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": { diff --git a/langs/layers/nl.json b/langs/layers/nl.json index 0bb510456..e84be8c4c 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -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 met de hand aangepast" + }, + "1": { + "then": "De verzorgingstafel wordt door een electrische motor 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 met wielen en kan verplaatst worden" + } + }, + "question": "Hoe is de verschoningstafel in de ruimte geplaatst?" + }, + "height": { + "mappings": { + "0": { + "then": "De verzorgingstafel is in hoogte verstelbaar" + } + }, + "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 in hoogte verstelbaar" - } - }, - "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 met de hand aangepast" - }, - "1": { - "then": "De verzorgingstafel wordt door een electrische motor 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 met wielen en kan verplaatst worden" - } - }, - "question": "Hoe is de verschoningstafel in de ruimte geplaatst?" - }, - "adult-changing-table-title": { - "render": "

Verzorgingstafel voor volwassenen

" - }, "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": { diff --git a/langs/layers/pt.json b/langs/layers/pt.json index b1b29029b..902d89b30 100644 --- a/langs/layers/pt.json +++ b/langs/layers/pt.json @@ -1765,7 +1765,7 @@ } } }, - "toilet_at_amenity": { + "toilet": { "tagRenderings": { "toilets-fee": { "mappings": { diff --git a/langs/layers/ru.json b/langs/layers/ru.json index 2581c2fbf..9a7936e85 100644 --- a/langs/layers/ru.json +++ b/langs/layers/ru.json @@ -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": { diff --git a/langs/layers/sl.json b/langs/layers/sl.json index 3e497d7be..d2cfb23b0 100644 --- a/langs/layers/sl.json +++ b/langs/layers/sl.json @@ -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": { diff --git a/langs/layers/uk.json b/langs/layers/uk.json index 6f0b2784c..2975bcd8c 100644 --- a/langs/layers/uk.json +++ b/langs/layers/uk.json @@ -2491,9 +2491,6 @@ "override": { "question": "Коли відкрито приміщення, де розташовані ці туалети?" } - }, - "toilet-access": { - "render": "Доступ - {toilets:access}" } } }, diff --git a/package.json b/package.json index 637590a7b..10d93f811 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 2ac8e2f36..7981d96ad 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -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 { diff --git a/scripts/generateFavouritesLayer.ts b/scripts/generateFavouritesLayer.ts index ad78cd1ea..ef4cbff57 100644 --- a/scripts/generateFavouritesLayer.ts +++ b/scripts/generateFavouritesLayer.ts @@ -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 = JSON.parse(readFileSync(path, "utf8")) for (const tagRendering of config.tagRenderings) { diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index edeb49146..93e83d907 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -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> { + private readonly _dependencies: ReadonlyMap + private readonly _states: Map + private readonly prepareLayer: PrepareLayer + private readonly _levels: LevelInfo[] + private readonly _loadedIds: Set = new Set() + private readonly _layerConfigJsons = new Map + private readonly _desugaringState: DesugaringContext + + constructor( + layerConfigJsons: LayerConfigJson[], + dependencies: Map, + levels: LevelInfo[], + states: Map, + 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" (submachine 4) + 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 = [...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 { + + 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 { @@ -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() @@ -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(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 + doesImageExist: DoesImageExist ): Map { // 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() - 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() + 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( diff --git a/scripts/initFiles.sh b/scripts/initFiles.sh index 52bfc3201..09bca4b7a 100755 --- a/scripts/initFiles.sh +++ b/scripts/initFiles.sh @@ -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 diff --git a/scripts/prepare-build.sh b/scripts/prepare-build.sh index 88bb11ee7..7155e3467 100755 --- a/scripts/prepare-build.sh +++ b/scripts/prepare-build.sh @@ -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 diff --git a/src/Customizations/AllKnownLayouts.ts b/src/Customizations/AllKnownLayouts.ts index 3734c1398..d2cd5feca 100644 --- a/src/Customizations/AllKnownLayouts.ts +++ b/src/Customizations/AllKnownLayouts.ts @@ -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" diff --git a/src/Logic/DetermineTheme.ts b/src/Logic/DetermineTheme.ts index 299660695..fed03a7ce 100644 --- a/src/Logic/DetermineTheme.ts +++ b/src/Logic/DetermineTheme.ts @@ -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" diff --git a/src/Logic/Search/GeocodingProvider.ts b/src/Logic/Search/GeocodingProvider.ts index ebd2b9d8a..0ea017a47 100644 --- a/src/Logic/Search/GeocodingProvider.ts +++ b/src/Logic/Search/GeocodingProvider.ts @@ -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" diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index 8713a1ce0..1508bea92 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -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" diff --git a/src/Logic/State/UserSettingsMetaTagging.ts b/src/Logic/State/UserSettingsMetaTagging.ts index 6e568c5c3..33a5ae85b 100644 --- a/src/Logic/State/UserSettingsMetaTagging.ts +++ b/src/Logic/State/UserSettingsMetaTagging.ts @@ -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 }) { - 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(/</g, "<")?.replace(/>/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}) { + 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(/</g,'<')?.replace(/>/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' + } +} \ No newline at end of file diff --git a/src/Models/ThemeConfig/Conversion/AddPrefixToTagRenderingConfig.ts b/src/Models/ThemeConfig/Conversion/AddPrefixToTagRenderingConfig.ts new file mode 100644 index 000000000..6aa347207 --- /dev/null +++ b/src/Models/ThemeConfig/Conversion/AddPrefixToTagRenderingConfig.ts @@ -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 { + + + 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 = {} + 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[] { + return tags?.map(tag => this.updateTag(tag)) + } + + private updateMapping(mapping: Readonly): 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, 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 { + ...json, + id: this._prefix + "_" + json.id, + + question: this.updateTranslatable(json.question), + questionHint: this.updateTranslatable(json.questionHint), + + render: this.updateTranslatable(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 + } + } + + +} diff --git a/src/Models/ThemeConfig/Conversion/ExpandFilter.ts b/src/Models/ThemeConfig/Conversion/ExpandFilter.ts index 43bc4735c..7f828f809 100644 --- a/src/Models/ThemeConfig/Conversion/ExpandFilter.ts +++ b/src/Models/ThemeConfig/Conversion/ExpandFilter.ts @@ -239,11 +239,11 @@ export class ExpandFilter extends DesugaringStep { ) } 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) { diff --git a/src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts b/src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts index df1697c87..dae762b2b 100644 --- a/src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts +++ b/src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts @@ -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 = layer.title + return [{ + ...titleTr, + id: layer.id + "_title" + }] + } else { + const transl = 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, 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"] diff --git a/src/Models/ThemeConfig/Conversion/PrepareLayer.ts b/src/Models/ThemeConfig/Conversion/PrepareLayer.ts index a3e68a0ca..72ec442c3 100644 --- a/src/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/src/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -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 { 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 { if (json === undefined || json === null) { throw "Error: prepareLayer got null" } + if (json.source?.["osmTags"] !== undefined && json.source?.["osmTags"]?.["and"] === undefined) { + json = { ...json } + json.source = { ...(json.source) } + json.source["osmTags"] = { "and": [json.source["osmTags"]] } + } return super.convert(json, context) } } diff --git a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts index 261f6fbf8..8dfd15376 100644 --- a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts +++ b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts @@ -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 !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 diff --git a/src/Models/ThemeConfig/Json/LayerConfigJson.ts b/src/Models/ThemeConfig/Json/LayerConfigJson.ts index 533911ae8..79c4abe7a 100644 --- a/src/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/src/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -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: ".title" will return the layer's title for an element + */ builtin: string | string[] - override: Partial + override: Partial, + /** + * 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< diff --git a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts index 37da3a4c3..f7d9e957a 100644 --- a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts +++ b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts @@ -289,7 +289,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs * Extra arguments to configure the input element * group: hidden */ - helperArgs: any + helperArgs?: any } /** diff --git a/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts b/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts index 144c60ebe..e33798d9b 100644 --- a/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts +++ b/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts @@ -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 diff --git a/src/Models/ThemeConfig/LayerConfigDependencyGraph.ts b/src/Models/ThemeConfig/LayerConfigDependencyGraph.ts new file mode 100644 index 000000000..f15a4c51d --- /dev/null +++ b/src/Models/ThemeConfig/LayerConfigDependencyGraph.ts @@ -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 = ["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 = tr["builtin"] + if (typeof builtin === "string") { + importedTrs.push(builtin) + } else { + importedTrs.push(...builtin) + } + } + } + + const imports = new Set(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() + for (const layer of layers) { + const layerDependsOn = LayerConfigDependencyGraph.getLayerImports(layer) + dependsOn.set(layer.id, layerDependsOn) + } + return dependsOn + } + public static buildLevels(dependsOn: Map): LevelInfo[]{ + + const levels: LevelInfo[] = [] + const seenIds = new Set() + while (Array.from(dependsOn.keys()).length > 0) { + const currentLevel: LevelInfo = { + ids: [], + } + 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() + 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 + + } +} diff --git a/src/Models/ThemeViewState/WithSpecialLayers.ts b/src/Models/ThemeViewState/WithSpecialLayers.ts index ea0af40f1..c5ca0d1f1 100644 --- a/src/Models/ThemeViewState/WithSpecialLayers.ts +++ b/src/Models/ThemeViewState/WithSpecialLayers.ts @@ -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 */ diff --git a/src/UI/BigComponents/MenuDrawer.svelte b/src/UI/BigComponents/MenuDrawer.svelte index 06c8c9cc3..5ca7a8fdd 100644 --- a/src/UI/BigComponents/MenuDrawer.svelte +++ b/src/UI/BigComponents/MenuDrawer.svelte @@ -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" diff --git a/src/UI/Flowbite/AccordionSingle.svelte b/src/UI/Flowbite/AccordionSingle.svelte index 4746afae7..7c9fedcdd 100644 --- a/src/UI/Flowbite/AccordionSingle.svelte +++ b/src/UI/Flowbite/AccordionSingle.svelte @@ -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 @@ - + diff --git a/src/UI/Image/NearbyImages.svelte b/src/UI/Image/NearbyImages.svelte index 81fbdac69..5cfff3742 100644 --- a/src/UI/Image/NearbyImages.svelte +++ b/src/UI/Image/NearbyImages.svelte @@ -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" diff --git a/src/UI/OpeningHours/OpeningHours.ts b/src/UI/OpeningHours/OpeningHours.ts index 527c793fd..cfb261d16 100644 --- a/src/UI/OpeningHours/OpeningHours.ts +++ b/src/UI/OpeningHours/OpeningHours.ts @@ -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]) diff --git a/src/UI/OpeningHours/OpeningHoursVisualization.ts b/src/UI/OpeningHours/OpeningHoursVisualization.ts index c513a44dd..73f2c6a74 100644 --- a/src/UI/OpeningHours/OpeningHoursVisualization.ts +++ b/src/UI/OpeningHours/OpeningHoursVisualization.ts @@ -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() } } diff --git a/src/UI/Popup/GroupedView.svelte b/src/UI/Popup/GroupedView.svelte index 3255509f0..cb3405d42 100644 --- a/src/UI/Popup/GroupedView.svelte +++ b/src/UI/Popup/GroupedView.svelte @@ -13,6 +13,7 @@ export let selectedElement: Feature export let tags: UIEventSource 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() + 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) diff --git a/src/UI/Popup/TagRendering/Questionbox.svelte b/src/UI/Popup/TagRendering/Questionbox.svelte index 152b0978b..4c9ed9ffe 100644 --- a/src/UI/Popup/TagRendering/Questionbox.svelte +++ b/src/UI/Popup/TagRendering/Questionbox.svelte @@ -226,7 +226,9 @@ {/if} {#if $debug} - Skipped questions are {Array.from($skippedQuestions).join(", ")} + + DBG:Skipped questions are {Array.from($skippedQuestions).join(", ")} + {/if} diff --git a/src/UI/SpecialVisualisations/ImageVisualisations.ts b/src/UI/SpecialVisualisations/ImageVisualisations.ts index f3a56239a..0517950e4 100644 --- a/src/UI/SpecialVisualisations/ImageVisualisations.ts +++ b/src/UI/SpecialVisualisations/ImageVisualisations.ts @@ -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 image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, 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", diff --git a/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts b/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts index 9700e6af6..62e5dd633 100644 --- a/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts +++ b/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts @@ -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 }) }, }, diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index 2e89f4e00..8f6cd8f04 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -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[] diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 0c1c89f30..43afabe9c 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -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>, - 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>, 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", }, diff --git a/src/Utils.ts b/src/Utils.ts index b705d51cd..5df75124f 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -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() } diff --git a/src/index.css b/src/index.css index ca22b0957..55cb6b9dd 100644 --- a/src/index.css +++ b/src/index.css @@ -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 {