Merge develop

This commit is contained in:
Pieter Vander Vennet 2025-08-31 15:21:28 +02:00
commit 0969558b97
74 changed files with 1785 additions and 637 deletions

View file

@ -0,0 +1,115 @@
{
"id": "arcade",
"name": {
"en": "Arcades"
},
"description": {
"en": "Layer showing arcades"
},
"source": {
"osmTags": "leisure=amusement_arcade"
},
"minzoom": 10,
"title": {
"render": {
"en": "Arcade"
},
"mappings": [
{
"if": "name~*",
"then": {
"*": "{name}"
}
}
]
},
"pointRendering": [
{
"location": [
"point",
"centroid"
],
"marker": [
{
"icon": "square",
"color": "white"
},
{
"icon": "./assets/layers/arcade/arcade.svg"
}
]
}
],
"lineRendering": [
{
"width": 3,
"color": "#0e8517"
}
],
"presets": [
{
"title": {
"en": "an arcade"
},
"tags": [
"leisure=amusement_arcade"
]
}
],
"tagRenderings": [
"images",
"reviews",
{
"builtin": "name",
"override": {
"question": {
"en": "What is the name of this arcade?"
},
"render": {
"en": "This arcade is called <b>{name}</b>"
}
}
},
{
"id": "virtual_reality",
"question": {
"en": "Does this arcade offer virtual-reality gaming?"
},
"mappings": [
{
"if": "virtual_reality=yes",
"then": {
"en": "This arcade offers virtual-reality gaming."
}
},
{
"if": "virtual_reality=only",
"then": {
"en": "This arcade <b>only</b> offers virtual-reality gaming."
}
},
{
"if": "virtual_reality=",
"then": {
"en": "This arcade doesn't offer virtual-reality gaming"
}
}
]
},
"brand",
"opening_hours",
"website",
"email",
"phone",
"payment-options",
"level",
"description",
"toilet_at_amenity_lib.all"
],
"allowMove": {
"enableImproveAccuracy": true,
"enableRelocation": true
},
"credits": "Robin van der Linde",
"credits:uid": 5093765
}

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<path fill="#0e8517" d="m0,1 h4 v2 h-4z m10,0 h4 v2 h-4z m-6,2 v2 h-2 v2 h-2 v7 h2 v-2 h2 v2 h2 v-2 h2 v2 h2 v-2 h2 v2 h2 v-7 h-2 v-2 h-2 v-2 h-2 v2 h-2 v-2z m0,4 h2 v2 h-2 z m4,0 h2 v2 h-2 z"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: meased
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,12 @@
[
{
"path": "arcade.svg",
"license": "CC0-1.0",
"authors": [
"meased"
],
"sources": [
"https://github.com/gravitystorm/openstreetmap-carto/blob/master/symbols/leisure/amusement_arcade.svg"
]
}
]

View file

@ -280,7 +280,6 @@
] ]
} }
}, },
"caravansites.caravansites-toilets",
"toilet_at_amenity_lib.all", "toilet_at_amenity_lib.all",
"questions", "questions",
"mastodon" "mastodon"

View file

@ -666,85 +666,7 @@
] ]
} }
}, },
{ "has_toilets",
"id": "caravansites-toilets",
"question": {
"en": "Does this place have toilets?",
"ca": "Aquest lloc té lavabos?",
"cs": "Má toto místo toalety?",
"da": "Har dette sted toiletter?",
"de": "Verfügt dieser Ort über Toiletten?",
"es": "¿Este lugar tiene baños?",
"fr": "Y-a-til des toilettes sur le site ?",
"hu": "Van-e itt WC?",
"it": "Questo posto ha servizi igienici?",
"ja": "ここにトイレはありますか?",
"nb_NO": "Har dette stedet toaletter?",
"nl": "Heeft deze plaats toiletten?",
"pl": "Czy to miejsce ma toalety?",
"pt": "Este lugar tem casas de banho?",
"pt_BR": "Este lugar tem banheiros?",
"ru": "Здесь есть туалеты?",
"zh_Hant": "這個地方有廁所嗎?"
},
"mappings": [
{
"if": {
"and": [
"toilets=yes"
]
},
"then": {
"en": "This place has toilets",
"ca": "Aquest lloc té lavabos",
"cs": "Toto místo má toalety",
"da": "Dette sted har toiletter",
"de": "Dieser Ort verfügt über Toiletten",
"es": "Este lugar tiene baños",
"fr": "Ce site a des toilettes",
"hu": "Itt van WC",
"id": "Tempat sini ada tandas",
"it": "Questo posto ha servizi igienici",
"ja": "ここにはトイレがある",
"nb_NO": "Dette stedet har toalettfasiliteter",
"nl": "Deze plaats heeft toiletten",
"pl": "To miejsce ma toalety",
"pt": "Este lugar tem casa de banho",
"pt_BR": "Este lugar tem banheiros",
"ru": "В этом месте есть туалеты",
"zh_Hant": "這個地方有廁所"
}
},
{
"if": {
"and": [
"toilets=no"
]
},
"then": {
"en": "This place does not have toilets",
"ca": "Aquest lloc no té lavabos",
"cs": "Toto místo nemá toalety",
"da": "Dette sted har ikke toiletter",
"de": "Dieser Ort verfügt nicht über Toiletten",
"es": "Este lugar no tiene baños",
"eu": "Toki honek ez dauka komunik",
"fr": "Ce site na pas de toilettes",
"hu": "Itt nincs WC",
"id": "Tempat sini tiada tandas",
"it": "Questo posto non ha servizi igienici",
"ja": "ここにはトイレがない",
"nb_NO": "Dette stedet har ikke toalettfasiliteter",
"nl": "Deze plaats heeft geen toiletten",
"pl": "To miejsce nie ma toalet",
"pt": "Este lugar não tem casas de banho",
"pt_BR": "Este lugar não tem banheiros",
"ru": "В этом месте нет туалетов",
"zh_Hant": "這個地方並沒有廁所"
}
}
]
},
{ {
"id": "caravansites-website", "id": "caravansites-website",
"question": { "question": {

View file

@ -571,6 +571,28 @@
} }
} }
] ]
},
{
"id": "shelter",
"options": [
{
"osmTags": {
"or": [
"shelter=yes",
"shelter=separate"
]
},
"question": {
"en": "With a shelter",
"ca": "Amb refugi",
"cs": "S přístřeškem",
"de": "Mit Unterstand",
"es": "Con refugio",
"fr": "Avec un abri",
"it": "Con una pensilina"
}
}
]
} }
], ],
"allowMove": false "allowMove": false

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="14" height="14" viewBox="0 0 14 14" id="svg2">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs id="defs6"/>
<rect width="14" height="14" x="0" y="0" id="canvas" style="fill:none;stroke:none;visibility:hidden"/>
<path id="path757" d="M 7 0 L 0 3.5 L 0 5.5 L 1.5 4.8496094 L 1.5 14 L 3 14 L 3 4.1992188 L 7 2.5 L 11 4.1992188 L 11 14 L 12.5 14 L 12.5 4.8496094 L 14 5.5 L 14 3.5 L 7 0 z M 6.9726562 5.0488281 C 6.5840971 5.0488281 6.2675781 5.3630448 6.2675781 5.7519531 C 6.2675781 6.1417342 6.584097 6.4550781 6.9726562 6.4550781 C 7.361041 6.4550781 7.6757812 6.1417342 7.6757812 5.7519531 C 7.6757812 5.3630448 7.3608664 5.0488281 6.9726562 5.0488281 z M 5.0488281 6.1875 C 4.9393822 6.162539 4.8312529 6.2261094 4.8066406 6.3339844 L 4.3144531 8.375 C 4.2879211 8.483224 4.3541425 8.5899565 4.4628906 8.6171875 L 5.0488281 8.7578125 C 5.1556558 8.7859125 5.2664033 8.7195541 5.2910156 8.6113281 L 5.7832031 6.5722656 C 5.8095609 6.4640416 5.7430227 6.355181 5.6367188 6.328125 L 5.0488281 6.1875 z M 6.7675781 6.5644531 C 6.1938161 6.5499631 6.0488281 7.1523437 6.0488281 7.1523438 L 4.5605469 13.40625 C 4.5553069 13.43959 4.5527344 13.472519 4.5527344 13.505859 C 4.5527344 13.78026 4.7744278 14 5.0488281 14 C 5.2720838 14 5.4610884 13.85019 5.5214844 13.646484 L 6.3730469 10.126953 L 7.1816406 13.625 C 7.2340066 13.841273 7.4308241 14 7.6621094 14 C 7.9354624 14 8.1542969 13.780085 8.1542969 13.505859 C 8.1542969 13.468499 8.1525944 13.431918 8.1464844 13.396484 L 7.0332031 8.7480469 L 7.1269531 8.3398438 L 7.1992188 8.6621094 C 7.2652007 8.8720994 7.4570312 8.890625 7.4570312 8.890625 L 8.6015625 9.1835938 C 8.6249525 9.1875937 8.648315 9.1894531 8.671875 9.1894531 C 8.859346 9.1894531 9.0097656 9.0406386 9.0097656 8.8535156 C 9.0097656 8.6934486 8.900292 8.5569194 8.75 8.5214844 L 7.7578125 8.2734375 L 7.4863281 7.1660156 C 7.3564592 6.5486156 6.7675781 6.5644531 6.7675781 6.5644531 z M 9.8300781 7.2792969 C 9.7478581 7.2792969 9.6872029 7.3408519 9.6699219 7.4199219 L 8.4882812 13.826172 C 8.4882812 13.830372 8.4863281 13.833837 8.4863281 13.835938 C 8.4863281 13.925487 8.5592405 13.998047 8.6484375 13.998047 C 8.7296075 13.998047 8.7993749 13.935444 8.8105469 13.857422 L 9.9902344 7.4511719 L 9.9902344 7.4394531 C 9.9902344 7.3521731 9.9192711 7.2792969 9.8300781 7.2792969 z " style="fill:#0092da;stroke-width:0.17455491"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Geozeisig
SPDX-License-Identifier: CC0-1.0

179
assets/layers/hut/hut.json Normal file
View file

@ -0,0 +1,179 @@
{
"id": "hut",
"name": {
"en": "Huts"
},
"description": {
"en": "Layer showing basic huts, wilderness huts and alpine huts"
},
"source": {
"osmTags": {
"or": [
"tourism=wilderness_hut",
"tourism=alpine_hut",
{
"and": [
"amenity=shelter",
"shelter_type=basic_hut"
]
}
]
}
},
"minzoom": 10,
"title": {
"render": "Hut",
"mappings": [
{
"if": "name~*",
"then": "{name}"
},
{
"if": "tourism=wilderness_hut",
"then": "wilderness hut"
},
{
"if": "tourism=alpine_hut",
"then": "alpine hut"
},
{
"if": {
"and": [
"amenity=shelter",
"shelter_type=basic_hut"
]
},
"then": "basic hut"
}
]
},
"pointRendering": [
{
"location": [
"point",
"centroid"
],
"marker": [
{
"icon": {
"render": "./assets/layers/shelter/shelter.svg",
"mappings": [
{
"if": "tourism=wilderness_hut",
"then": "./assets/layers/hut/wilderness_hut.svg"
},
{
"if": "tourism=alpine_hut",
"then": "./assets/layers/hut/alpine_hut.svg"
},
{
"if": {
"and": [
"amenity=shelter",
"shelter_type=basic_hut"
]
},
"then": "./assets/layers/shelter/shelter.svg"
}
]
}
}
]
}
],
"lineRendering": [],
"presets": [
{
"tags": [
"tourism=wilderness_hut"
],
"title": {
"en": "wilderness hut"
},
"description": {
"en": "An unserviced fully enclosed hut (with roof and walls) with beds or suitable sleeping areas and a fireplace or stove for heating and cooking."
}
},
{
"tags": [
"tourism=alpine_hut"
],
"title": {
"en": "alpine hut"
},
"description": {
"en": "A serviced remote building located in the mountains intended to provide board and lodging."
}
},
{
"tags": [
"amenity=shelter",
"shelter_type=basic_hut"
],
"title": {
"en": "basic hut"
},
"description": {
"en": "An unserviced fully enclosed hut (with roof and walls) with beds or suitable sleeping areas <b>without</b> a fireplace or stove."
}
}
],
"tagRenderings": [
"images",
"name",
{
"builtin": "website",
"override": {
"condition": "tourism=wilderness_hut",
"id": "website-single"
}
},
{
"builtin": "contact",
"override": {
"condition": "tourism=alpine_hut"
}
},
"reservation",
"caravansites.caravansites-fee",
{
"id": "drinking_water",
"question": {
"en": "Is drinking water available here?"
},
"mappings": [
{
"if": "drinking_water=yes",
"then": {
"en": "Here is drinking water available."
}
},
{
"if": "drinking_water=no",
"then": {
"en": "Here is no drinking water available."
}
}
]
},
"has_toilets",
"description",
{
"id": "preset_type",
"render": "{preset_type_select()}"
},
{
"builtin": "shelter.shelter-type",
"override": {
"condition": "amenity=shelter"
}
}
],
"filter":[
"free"
],
"allowMove": {
"enableRelocation": false,
"enableImproveAccuracy": true
}
}

View file

@ -0,0 +1,22 @@
[
{
"path": "alpine_hut.svg",
"license": "CC0-1.0",
"authors": [
"Geozeisig"
],
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Alpinehut.svg"
]
},
{
"path": "wilderness_hut.svg",
"license": "CC0-1.0",
"authors": [
"Geozeisig"
],
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Wilderness_hut.svg"
]
}
]

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="14" height="14" viewBox="0 0 14 14" id="svg2">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs id="defs6"/>
<rect width="14" height="14" x="0" y="0" id="canvas" style="fill:none;stroke:none;visibility:hidden"/>
<path id="wilderness-hut" d="M 8,0 8,2 7,1.5 0,5 0,7 1.5,6.3496094 1.5,14 3,14 3,5.6992188 7,4 8,4.4257812 8,9 4.5,9 c 0.025314,1.65157 -0.034277,3.38952 0,5 l 5,0 C 9.518206,11.046736 9.50603,8.0503867 9.5,5.0625 L 11,5.6992188 11,14 12.5,14 12.5,6.3496094 14,7 14,5 9.5,2.75 C 9.4984477,1.8314022 9.5,0.9158261 9.5,0 L 8,0 Z M 7,11 c 0,0 1,0 1,1 l 0,1 -2,0 0,-1 c 0,-1 1,-1 1,-1 z" style="fill:#0092da;fill-opacity:1;stroke:none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Geozeisig
SPDX-License-Identifier: CC0-1.0

View file

@ -69,6 +69,15 @@
"icon": { "icon": {
"render": "./assets/layers/parking_spaces/parking_space.svg", "render": "./assets/layers/parking_spaces/parking_space.svg",
"mappings": [ "mappings": [
{
"if": {
"or": [
"access=private",
"access=no"
]
},
"then": "./assets/layers/parking_spaces/parking_space_private.svg"
},
{ {
"if": "parking_space=disabled", "if": "parking_space=disabled",
"then": "./assets/layers/toilet/wheelchair.svg" "then": "./assets/layers/toilet/wheelchair.svg"
@ -99,7 +108,7 @@
], ],
"lineRendering": [ "lineRendering": [
{ {
"color": "#696969", "color": "dimgray",
"width": "1" "width": "1"
} }
], ],
@ -295,6 +304,69 @@
"it": "Questo è un posto auto riservato al car sharing.", "it": "Questo è un posto auto riservato al car sharing.",
"nl": "Deze parkeerplek is gereserveerd voor autodelen." "nl": "Deze parkeerplek is gereserveerd voor autodelen."
} }
},
{
"if": "parking_space=women",
"then": {
"en": "This is a parking space reserved for women.",
"nl": "Deze parkeerplek is gereserveerd voor vrouwen."
}
}
]
},
{
"id": "access",
"question": {
"en": "Who can use this parking space?",
"nl": "Wie mag deze parkeerplek gebruiken?"
},
"render": {
"en": "Access of parking space: {access}",
"nl": "Toegang tot parkeerplek: {access}"
},
"freeform": {
"key": "access",
"type": "string",
"addExtraTags": [
"fixme=Freeform used on 'access'-tag: possibly a wrong value"
]
},
"mappings": [
{
"if": "access=",
"then": {
"en": "Anyone can use this parking space.",
"nl": "Iedereen kan deze parkeerplek gebruiken."
},
"hideInAnswer": true
},
{
"if": "access=yes",
"then": {
"en": "Anyone can use this parking space.",
"nl": "Iedereen kan deze parkeerplek gebruiken."
}
},
{
"if": "access=customers",
"then": {
"en": "This parking space is reserved for customers.",
"nl": "Deze parkeerplek is gereserveerd voor klanten."
}
},
{
"if": "access=private",
"then": {
"en": "This parking space is private and cannot be used by the general public.",
"nl": "Deze parkeerplek is privé en mag niet door het grote publiek worden gebruikt."
}
},
{
"if": "access=permit",
"then": {
"en": "This parking space is reserved for permit holders.",
"nl": "Deze parkeerplek is gereserveerd voor vergunninghouders."
}
} }
] ]
}, },
@ -310,6 +382,19 @@
"nl": "Deze parkeerplek heeft {capacity} plaatsen." "nl": "Deze parkeerplek heeft {capacity} plaatsen."
}, },
"mappings": [ "mappings": [
{
"if": "capacity=",
"then": {
"en": "This parking space has 1 space.",
"ca": "Aquest espai d'aparcament té 1 plaça.",
"cs": "Toto parkoviště má 1 místo.",
"de": "Dieser Parkplatz hat 1 Stellplatz.",
"es": "Esta plaza de aparcamiento tiene 1 plaza.",
"it": "Questo posto auto ha 1 spazio.",
"nl": "Deze parkeerplek heeft 1 plaats."
},
"hideInAnswer": true
},
{ {
"if": "capacity=1", "if": "capacity=1",
"then": { "then": {
@ -329,4 +414,4 @@
"enableImproveAccuracy": true, "enableImproveAccuracy": true,
"enableRelocation": false "enableRelocation": false
} }
} }

View file

@ -0,0 +1,304 @@
{
"id": "picnic_site",
"name": {
"en": "Picnic sites",
"nl": "Picknickplaatsen"
},
"description": {
"en": "Picnic sites for eating outdoors, featuring amenities like toilets, water taps, BBQ, benches and shelters",
"nl": "Picknickplaatsen voor het eten in de buitenlucht, met voorzieningen zoals toiletten, waterkranen, BBQ, banken en schuilplaatsen"
},
"source": {
"osmTags": "tourism=picnic_site"
},
"minzoom": 10,
"title": {
"render": {
"en": "Picnic site",
"nl": "Picknickplaats"
}
},
"pointRendering": [
{
"iconSize": "35,35",
"location": [
"point",
"centroid"
],
"anchor": "center",
"marker": [
{
"color": "#3984e6",
"icon": "circle"
},
{
"icon": "./assets/layers/picnic_table/picnic_table.svg"
}
]
}
],
"lineRendering": [
{
"color": "#3984e6",
"fillColor": "#3984e6bd",
"width": 5
}
],
"presets": [
{
"tags": [
"tourism=picnic_site"
],
"title": {
"en": "a picnic site",
"nl": "een picknickplaats"
},
"description": {
"en": "A picnic site for eating outdoors, featuring amenities like toilets, water taps, BBQ, benches and shelters",
"nl": "Een picknickplaats voor het eten in de buitenlucht, met voorzieningen zoals toiletten, waterkranen, BBQ, banken en schuilplaatsen"
}
}
],
"tagRenderings": [
"images",
{
"builtin": "name",
"override": {
"render": {
"en": "This picnic site is called {name}",
"nl": "Deze picknickplaats heet {name}"
}
}
},
{
"id": "shelter",
"question": {
"en": "Does this picnic site have a shelter?",
"nl": "Heeft deze picknickplaats een schuilplaats?"
},
"mappings": [
{
"if": "shelter=yes",
"then": {
"en": "This picnic site has a shelter.",
"nl": "Deze picknickplaats heeft een schuilplaats."
}
},
{
"if": "shelter=no",
"then": {
"en": "This picnic site does not have a shelter.",
"nl": "Deze picknickplaats heeft geen schuilplaats."
}
},
{
"if": "shelter=separate",
"then": {
"en": "This picnic site has a shelter, but is is mapped as a different icon.",
"nl": "Deze picknickplaats heeft een schuilplaats, maar deze staat los op de kaart."
}
}
]
},
{
"id": "fireplace",
"question": {
"en": "Does this picnic site have a firepit?",
"nl": "Heeft deze picknickplaats een vuurplaats?"
},
"mappings": [
{
"if": "fireplace=yes",
"then": {
"en": "This picnic site has a firepit.",
"nl": "Deze picknickplaats heeft een vuurplaats."
}
},
{
"if": "fireplace=no",
"then": {
"en": "This picnic site does not have a firepit.",
"nl": "Deze picknickplaats heeft geen vuurplaats."
}
},
{
"if": "fireplace=separate",
"then": {
"en": "This picnic site has a firepit, but it is mapped as a different icon.",
"nl": "Deze picknickplaats heeft een vuurplaats, maar deze staat los op de kaart."
}
}
]
},
{
"id": "bbq",
"question": {
"en": "Does this picnic site have a BBQ?",
"nl": "Heeft deze picknickplaats een BBQ?"
},
"mappings": [
{
"if": "bbq=yes",
"then": {
"en": "This picnic site has a BBQ.",
"nl": "Deze picknickplaats heeft een BBQ."
}
},
{
"if": "bbq=no",
"then": {
"en": "This picnic site does not have a BBQ.",
"nl": "Deze picknickplaats heeft geen BBQ."
}
},
{
"if": "bbq=separate",
"then": {
"en": "This picnic site has a BBQ, but it is mapped as a different icon.",
"nl": "Deze picknickplaats heeft een BBQ, maar deze staat los op de kaart."
}
}
]
},
{
"id": "covered",
"question": {
"en": "Is this picnic site covered?",
"nl": "Is deze picknickplaats overdekt?"
},
"mappings": [
{
"if": "covered=yes",
"then": {
"en": "This picnic site is covered.",
"nl": "Deze picknickplaats is overdekt."
}
},
{
"if": "covered=no",
"then": {
"en": "This picnic site is not covered.",
"nl": "Deze picknickplaats is niet overdekt."
}
}
]
},
{
"id": "drinking_water",
"question": {
"en": "Does this picnic site have drinking water?",
"nl": "Heeft deze picknickplaats drinkwater?"
},
"mappings": [
{
"if": "drinking_water=yes",
"then": {
"en": "This picnic site has drinking water.",
"nl": "Deze picknickplaats heeft drinkwater."
}
},
{
"if": "drinking_water=no",
"then": {
"en": "This picnic site does not have drinking water.",
"nl": "Deze picknickplaats heeft geen drinkwater."
}
},
{
"if": "drinking_water=separate",
"then": {
"en": "This picnic site has drinking water, but it is mapped as a different icon.",
"nl": "Deze picknickplaats heeft drinkwater, maar deze staat los op de kaart."
}
}
]
},
{
"id": "openfire",
"question": {
"en": "Is open fire allowed at this picnic site?",
"nl": "Is open vuur toegestaan op deze picknickplaats?"
},
"mappings": [
{
"if": "openfire=yes",
"then": {
"en": "Open fire is allowed at this picnic site.",
"nl": "Open vuur is toegestaan op deze picknickplaats."
}
},
{
"if": "openfire=no",
"then": {
"en": "Open fire is not allowed at this picnic site.",
"nl": "Open vuur is niet toegestaan op deze picknickplaats."
}
},
{
"if": "openfire=permit",
"then": {
"en": "Open fire is allowed at this picnic site with a permit.",
"nl": "Open vuur is toegestaan op deze picknickplaats met een vergunning."
}
}
]
}
],
"filter": [
"shelter",
{
"id": "fireplace",
"options": [
{
"question": {
"en": "With a firepit",
"nl": "Met een vuurplaats"
},
"osmTags": {
"or": [
"fireplace=yes",
"fireplace=separate"
]
}
}
]
},
{
"id": "bbq",
"options": [
{
"question": {
"en": "With a BBQ",
"nl": "Met een BBQ"
},
"osmTags": {
"or": [
"bbq=yes",
"bbq=separate"
]
}
}
]
},
{
"id": "drinking_water",
"options": [
{
"question": {
"en": "With drinking water",
"nl": "Met drinkwater"
},
"osmTags": {
"or": [
"drinking_water=yes",
"drinking_water=separate"
]
}
}
]
}
],
"allowMove": {
"enableImproveAccuracy": true
}
}

View file

@ -3118,7 +3118,8 @@
"if": "nobrand=yes", "if": "nobrand=yes",
"addExtraTags": [ "addExtraTags": [
"brand=", "brand=",
"brand:wikidata=" "brand:wikidata=",
"brand:wikipedia="
], ],
"then": { "then": {
"en": "Not part of a bigger brand", "en": "Not part of a bigger brand",
@ -3657,4 +3658,4 @@
} }
} }
] ]
} }

View file

@ -43,7 +43,8 @@
"craft=key_cutter" "craft=key_cutter"
] ]
}, },
"shop!=mall" "shop!=mall",
"shop!=no"
] ]
} }
}, },
@ -486,7 +487,12 @@
"es": "Esta tienda no tiene una marca específica, no forma parte de una cadena más grande", "es": "Esta tienda no tiene una marca específica, no forma parte de una cadena más grande",
"it": "Questo negozio non ha un marchio specifico, non fa parte di una catena più grande", "it": "Questo negozio non ha un marchio specifico, non fa parte di una catena più grande",
"uk": "Цей магазин не має певного бренду, він не є частиною великої мережі" "uk": "Цей магазин не має певного бренду, він не є частиною великої мережі"
} },
"addExtraTags": [
"brand=",
"brand:wikidata=",
"brand:wikipedia="
]
} }
] ]
}, },

View file

@ -87,6 +87,10 @@
"if": "camera:type=doorbell", "if": "camera:type=doorbell",
"then": "./assets/layers/surveillance_camera/doorbell.svg" "then": "./assets/layers/surveillance_camera/doorbell.svg"
}, },
{
"if": "camera:type=panorama",
"then": "./assets/themes/surveillance/panorama.svg"
},
{ {
"if": "_direction:leftright=right", "if": "_direction:leftright=right",
"then": "./assets/themes/surveillance/cam_right.svg" "then": "./assets/themes/surveillance/cam_right.svg"
@ -110,6 +114,10 @@
] ]
}, },
"then": "50,35,center" "then": "50,35,center"
},
{
"if": "camera:type=panorama",
"then": "55,55,center"
} }
], ],
"render": "35,35,center" "render": "35,35,center"
@ -407,6 +415,16 @@
"ru": "Панорамная камера" "ru": "Панорамная камера"
} }
}, },
{
"if": "camera:type=panorama",
"icon": "./assets/themes/surveillance/panorama.svg",
"then": {
"en": "A 360° camera",
"de": "Eine 360°-Kamera",
"es": "Una cámara de 360°",
"fr": "Une caméra 360°"
}
},
{ {
"if": "camera:type=doorbell", "if": "camera:type=doorbell",
"icon": { "icon": {
@ -530,7 +548,7 @@
"da": "Hvilken form for overvågning er dette kamera?", "da": "Hvilken form for overvågning er dette kamera?",
"de": "Was überwacht diese Kamera?", "de": "Was überwacht diese Kamera?",
"es": "¿Qué tipo de vigilancia es esta cámara?", "es": "¿Qué tipo de vigilancia es esta cámara?",
"fr": "De quel genre de surveillance cette caméraest-elle ?", "fr": "De quel genre de surveillance cette caméra est-elle ?",
"it": "Che tipo di sorveglianza è questa telecamera?", "it": "Che tipo di sorveglianza è questa telecamera?",
"nl": "Wat soort bewaking wordt hier uitgevoerd?", "nl": "Wat soort bewaking wordt hier uitgevoerd?",
"sl": "Kaj nadzoruje ta kamera?" "sl": "Kaj nadzoruje ta kamera?"

View file

@ -548,28 +548,7 @@
} }
], ],
"filter": [ "filter": [
{ "shelter",
"id": "shelter",
"options": [
{
"osmTags": {
"or": [
"shelter=yes",
"shelter=separate"
]
},
"question": {
"en": "With a shelter",
"ca": "Amb refugi",
"cs": "S přístřeškem",
"de": "Mit Unterstand",
"es": "Con refugio",
"fr": "Avec un abri",
"it": "Con una pensilina"
}
}
]
},
{ {
"id": "bench", "id": "bench",
"options": [ "options": [
@ -617,4 +596,4 @@
"tactile_paving" "tactile_paving"
], ],
"allowMove": false "allowMove": false
} }

View file

@ -0,0 +1,13 @@
{
"id": "arcade",
"title": {
"en": "Arcades"
},
"description": {
"en": "A map of arcades"
},
"icon": "./assets/layers/arcade/arcade.svg",
"layers": [
"arcade"
]
}

View file

@ -38,6 +38,9 @@
"zh_Hant": "顯示由MapComplete進行的變動" "zh_Hant": "顯示由MapComplete進行的變動"
}, },
"icon": "./assets/svg/logo.svg", "icon": "./assets/svg/logo.svg",
"startZoom": 1,
"startLat": 0,
"startLon": 0,
"hideFromOverview": true, "hideFromOverview": true,
"layers": [ "layers": [
{ {

View file

@ -60,12 +60,14 @@
"nature_reserve", "nature_reserve",
{ {
"builtin": [ "builtin": [
"hut",
"shelter" "shelter"
], ],
"override": { "override": {
"minzoom": 11 "minzoom": 11
} }
}, },
"picnic_site",
{ {
"builtin": [ "builtin": [
"map", "map",

View file

@ -30,5 +30,13 @@
"Pieter Vander Vennet" "Pieter Vander Vennet"
], ],
"sources": [] "sources": []
},
{
"path": "panorama.svg",
"license": "CC0-1.0",
"authors": [
"Martin Bodin"
],
"sources": []
} }
] ]

View file

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="375px"
height="375px"
viewBox="0 0 375 375"
version="1.1"
id="svg11"
sodipodi:docname="panorama.svg"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata17">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs15" />
<sodipodi:namedview
pagecolor="#ffff00"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1005"
id="namedview13"
showgrid="false"
inkscape:zoom="1.4737475"
inkscape:cx="139.44044"
inkscape:cy="113.65583"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="surface1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<g
id="surface1">
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:4.36475;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
d="M 11.439583,18.537673 H 87.78125 c 12.54792,0.03646 12.93854,8.290452 0,8.132119 H 11.439583 c -11.3604163,0.03333 -11.219791,-8.256077 0,-8.132119 z"
id="path2"
transform="scale(3.75)"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:20.1633;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
d="m 34.717843,101.6866 c -1.27924,30.38368 1.929466,64.43545 24.151706,87.39173 20.294338,20.64802 52.480961,21.69978 78.731761,13.98482 26.2715,-8.688 54.77731,-12.42906 81.94054,-5.61303 20.40318,5.18001 41.09215,12.65231 62.49302,9.2938 25.93999,-2.45941 47.41513,-22.63158 54.71719,-47.13887 6.25508,-18.57863 7.37452,-38.46461 6.50031,-57.91845 -102.84484,0.015 -205.68968,0.015 -308.534527,0 z"
id="path4"
inkscape:connector-curvature="0" />
<g
id="g889-5"
transform="matrix(0.93922526,0,0,0.93922526,79.006064,2.8192093)">
<path
inkscape:connector-curvature="0"
id="path6-3"
d="m 138.94637,153.12112 c 0,12.14155 -10.48036,21.98508 -23.40956,21.98508 -12.9292,0 -23.40956,-9.84353 -23.40956,-21.98508 0,-12.14156 10.48036,-21.98509 23.40956,-21.98509 12.9292,0 23.40956,9.84353 23.40956,21.98509 z m 0,0"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.536273" />
<path
inkscape:connector-curvature="0"
id="path8-5"
d="m 129.9261,153.53588 c 0,7.78853 -6.53792,14.1044 -14.60506,14.1044 -8.06503,0 -14.60295,-6.31587 -14.60295,-14.1044 0,-7.78852 6.53792,-14.10228 14.60295,-14.10228 8.06714,0 14.60506,6.31376 14.60506,14.10228 z m 0,0"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.536273" />
</g>
<g
id="g889-5-6"
transform="matrix(0.69882528,0,0,0.93922526,220.19065,2.8192093)">
<path
inkscape:connector-curvature="0"
id="path6-3-2"
d="m 138.94637,153.12112 c 0,12.14155 -10.48036,21.98508 -23.40956,21.98508 -12.9292,0 -23.40956,-9.84353 -23.40956,-21.98508 0,-12.14156 10.48036,-21.98509 23.40956,-21.98509 12.9292,0 23.40956,9.84353 23.40956,21.98509 z m 0,0"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.536273" />
<path
inkscape:connector-curvature="0"
id="path8-5-9"
d="m 129.9261,153.53588 c 0,7.78853 -6.53792,14.1044 -14.60506,14.1044 -8.06503,0 -14.60295,-6.31587 -14.60295,-14.1044 0,-7.78852 6.53792,-14.10228 14.60295,-14.10228 8.06714,0 14.60506,6.31376 14.60506,14.10228 z m 0,0"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.536273" />
</g>
<g
id="g889-5-6-1"
transform="matrix(0.69882528,0,0,0.93922526,-6.6284342,2.8192093)">
<path
inkscape:connector-curvature="0"
id="path6-3-2-2"
d="m 138.94637,153.12112 c 0,12.14155 -10.48036,21.98508 -23.40956,21.98508 -12.9292,0 -23.40956,-9.84353 -23.40956,-21.98508 0,-12.14156 10.48036,-21.98509 23.40956,-21.98509 12.9292,0 23.40956,9.84353 23.40956,21.98509 z m 0,0"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.536273" />
<path
inkscape:connector-curvature="0"
id="path8-5-9-7"
d="m 129.9261,153.53588 c 0,7.78853 -6.53792,14.1044 -14.60506,14.1044 -8.06503,0 -14.60295,-6.31587 -14.60295,-14.1044 0,-7.78852 6.53792,-14.10228 14.60295,-14.10228 8.06714,0 14.60506,6.31376 14.60506,14.10228 z m 0,0"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.536273" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Martin Bodin
SPDX-License-Identifier: CC0-1.0

View file

@ -320,6 +320,7 @@
"menu": { "menu": {
"aboutCurrentThemeTitle": "About this map", "aboutCurrentThemeTitle": "About this map",
"aboutMapComplete": "About MapComplete", "aboutMapComplete": "About MapComplete",
"downloadApp": "Download the app for Android",
"filter": "Filter data", "filter": "Filter data",
"legal": "Legal notices", "legal": "Legal notices",
"moreUtilsTitle": "Discover more", "moreUtilsTitle": "Discover more",

View file

@ -2468,17 +2468,6 @@
}, },
"question": "Aquest lloc té una estació d'abocament sanitari?" "question": "Aquest lloc té una estació d'abocament sanitari?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Aquest lloc té lavabos"
},
"1": {
"then": "Aquest lloc no té lavabos"
}
},
"question": "Aquest lloc té lavabos?"
},
"caravansites-website": { "caravansites-website": {
"question": "Aquest lloc té un lloc web?", "question": "Aquest lloc té un lloc web?",
"render": "Lloc web oficial: <a href='{website}'>{website}</a>" "render": "Lloc web oficial: <a href='{website}'>{website}</a>"
@ -5510,6 +5499,13 @@
} }
} }
}, },
"23": {
"options": {
"0": {
"question": "Amb refugi"
}
}
},
"3": { "3": {
"options": { "options": {
"0": { "0": {
@ -7996,6 +7992,9 @@
"mappings": { "mappings": {
"0": { "0": {
"then": "Aquest espai d'aparcament té 1 plaça." "then": "Aquest espai d'aparcament té 1 plaça."
},
"1": {
"then": "Aquest espai d'aparcament té 1 plaça."
} }
}, },
"render": "Aquests espais d'aparcament tenen {capacity} places." "render": "Aquests espais d'aparcament tenen {capacity} places."
@ -11556,7 +11555,7 @@
"2": { "2": {
"then": "Una càmera panoràmica" "then": "Una càmera panoràmica"
}, },
"3": { "4": {
"then": "Un timbre que es pot activar remotament en qualsevol moment o mitjançant la detecció de moviment. Aquests són típicament <i>Smart</i>, banderes connectades a Internet. Les marques típiques són Ring, Google Nest, Eufy, ..." "then": "Un timbre que es pot activar remotament en qualsevol moment o mitjançant la detecció de moviment. Aquests són típicament <i>Smart</i>, banderes connectades a Internet. Les marques típiques són Ring, Google Nest, Eufy, ..."
} }
}, },
@ -12439,13 +12438,6 @@
"transit_stops": { "transit_stops": {
"description": "Capa que mostra diferents tipus de parades de transport públic.", "description": "Capa que mostra diferents tipus de parades de transport públic.",
"filter": { "filter": {
"0": {
"options": {
"0": {
"question": "Amb refugi"
}
}
},
"1": { "1": {
"options": { "options": {
"0": { "0": {

View file

@ -2663,17 +2663,6 @@
}, },
"question": "Má toto místo sanitární skládku?" "question": "Má toto místo sanitární skládku?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Toto místo má toalety"
},
"1": {
"then": "Toto místo nemá toalety"
}
},
"question": "Má toto místo toalety?"
},
"caravansites-website": { "caravansites-website": {
"question": "Má toto místo webové stránky?", "question": "Má toto místo webové stránky?",
"render": "Oficiální webové stránky: <a href='{website}'>{website}</a>" "render": "Oficiální webové stránky: <a href='{website}'>{website}</a>"
@ -5831,6 +5820,13 @@
} }
} }
}, },
"23": {
"options": {
"0": {
"question": "S přístřeškem"
}
}
},
"3": { "3": {
"options": { "options": {
"0": { "0": {
@ -8623,6 +8619,9 @@
"mappings": { "mappings": {
"0": { "0": {
"then": "Toto parkoviště má 1 místo." "then": "Toto parkoviště má 1 místo."
},
"1": {
"then": "Toto parkoviště má 1 místo."
} }
}, },
"render": "Toto parkoviště má {capacity} míst." "render": "Toto parkoviště má {capacity} míst."
@ -12500,7 +12499,7 @@
"2": { "2": {
"then": "Otáčecí kamera" "then": "Otáčecí kamera"
}, },
"3": { "4": {
"then": "Domovní zvonek, který lze spouštět kdykoli vzdáleně nebo detekcí pohybu. Jsou to typicky <i>chytré</i> zvonky připojené k Internetu. Typické značky jsou Ring, Google Nest, Eufy…" "then": "Domovní zvonek, který lze spouštět kdykoli vzdáleně nebo detekcí pohybu. Jsou to typicky <i>chytré</i> zvonky připojené k Internetu. Typické značky jsou Ring, Google Nest, Eufy…"
} }
}, },
@ -13546,13 +13545,6 @@
"transit_stops": { "transit_stops": {
"description": "Vrstva zobrazující různé typy zastávek veřejné dopravy.", "description": "Vrstva zobrazující různé typy zastávek veřejné dopravy.",
"filter": { "filter": {
"0": {
"options": {
"0": {
"question": "S přístřeškem"
}
}
},
"1": { "1": {
"options": { "options": {
"0": { "0": {

View file

@ -1561,17 +1561,6 @@
}, },
"question": "Har dette sted en sanitær tømningsstation?" "question": "Har dette sted en sanitær tømningsstation?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Dette sted har toiletter"
},
"1": {
"then": "Dette sted har ikke toiletter"
}
},
"question": "Har dette sted toiletter?"
},
"caravansites-website": { "caravansites-website": {
"question": "Har dette sted et websted?", "question": "Har dette sted et websted?",
"render": "Officiel hjemmeside: <a href='{website}'>{website}</a>" "render": "Officiel hjemmeside: <a href='{website}'>{website}</a>"
@ -3152,7 +3141,7 @@
"2": { "2": {
"then": "Et kamera, der panorerer" "then": "Et kamera, der panorerer"
}, },
"3": { "4": {
"then": "En dørklokke, som kan tændes på afstand når som helst eller ved bevægelsesregistrering. Disse er typisk <i>intelligente</i> internetforbundne dørklokker. Typiske mærker er Ring, Google Nest, Eufy, …" "then": "En dørklokke, som kan tændes på afstand når som helst eller ved bevægelsesregistrering. Disse er typisk <i>intelligente</i> internetforbundne dørklokker. Typiske mærker er Ring, Google Nest, Eufy, …"
} }
} }

View file

@ -2439,17 +2439,6 @@
}, },
"question": "Hat dieser Ort eine sanitäre Entsorgungsstation?" "question": "Hat dieser Ort eine sanitäre Entsorgungsstation?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Dieser Ort verfügt über Toiletten"
},
"1": {
"then": "Dieser Ort verfügt nicht über Toiletten"
}
},
"question": "Verfügt dieser Ort über Toiletten?"
},
"caravansites-website": { "caravansites-website": {
"question": "Hat dieser Ort eine Webseite?", "question": "Hat dieser Ort eine Webseite?",
"render": "Offizielle Webseite: <a href='{website}'>{website}</a>" "render": "Offizielle Webseite: <a href='{website}'>{website}</a>"
@ -5488,6 +5477,13 @@
} }
} }
}, },
"23": {
"options": {
"0": {
"question": "Mit Unterstand"
}
}
},
"3": { "3": {
"options": { "options": {
"0": { "0": {
@ -7960,6 +7956,9 @@
"mappings": { "mappings": {
"0": { "0": {
"then": "Dieser Parkplatz hat 1 Stellplatz." "then": "Dieser Parkplatz hat 1 Stellplatz."
},
"1": {
"then": "Dieser Parkplatz hat 1 Stellplatz."
} }
}, },
"render": "Dieser Parkplatz hat {capacity} Stellplätze." "render": "Dieser Parkplatz hat {capacity} Stellplätze."
@ -11546,6 +11545,9 @@
"then": "Eine bewegliche Kamera" "then": "Eine bewegliche Kamera"
}, },
"3": { "3": {
"then": "Eine 360°-Kamera"
},
"4": {
"then": "Eine Türklingel, die jederzeit oder per Bewegungserkennung ferngeschaltet werden kann. Dies sind typischerweise <i>Smart</i>, internetgebundene Türklingeln. Typische Marken sind Ring, Google Nest, Eufy, ..." "then": "Eine Türklingel, die jederzeit oder per Bewegungserkennung ferngeschaltet werden kann. Dies sind typischerweise <i>Smart</i>, internetgebundene Türklingeln. Typische Marken sind Ring, Google Nest, Eufy, ..."
} }
}, },
@ -12428,13 +12430,6 @@
"transit_stops": { "transit_stops": {
"description": "Ebene mit verschiedenen Arten von Haltestellen.", "description": "Ebene mit verschiedenen Arten von Haltestellen.",
"filter": { "filter": {
"0": {
"options": {
"0": {
"question": "Mit Unterstand"
}
}
},
"1": { "1": {
"options": { "options": {
"0": { "0": {

View file

@ -531,6 +531,40 @@
"render": "Animal shelter" "render": "Animal shelter"
} }
}, },
"arcade": {
"description": "Layer showing arcades",
"name": "Arcades",
"presets": {
"0": {
"title": "an arcade"
}
},
"tagRenderings": {
"name": {
"override": {
"question": "What is the name of this arcade?",
"render": "This arcade is called <b>{name}</b>"
}
},
"virtual_reality": {
"mappings": {
"0": {
"then": "This arcade offers virtual-reality gaming."
},
"1": {
"then": "This arcade <b>only</b> offers virtual-reality gaming."
},
"2": {
"then": "This arcade doesn't offer virtual-reality gaming"
}
},
"question": "Does this arcade offer virtual-reality gaming?"
}
},
"title": {
"render": "Arcade"
}
},
"artwork": { "artwork": {
"description": "An open map of statues, busts, graffitis and other artwork all over the world", "description": "An open map of statues, busts, graffitis and other artwork all over the world",
"name": "Artworks", "name": "Artworks",
@ -2663,17 +2697,6 @@
}, },
"question": "Does this place have a sanitary dump station?" "question": "Does this place have a sanitary dump station?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "This place has toilets"
},
"1": {
"then": "This place does not have toilets"
}
},
"question": "Does this place have toilets?"
},
"caravansites-website": { "caravansites-website": {
"question": "Does this place have a website?", "question": "Does this place have a website?",
"render": "Official website: <a href='{website}'>{website}</a>" "render": "Official website: <a href='{website}'>{website}</a>"
@ -5838,6 +5861,13 @@
} }
} }
}, },
"23": {
"options": {
"0": {
"question": "With a shelter"
}
}
},
"3": { "3": {
"options": { "options": {
"0": { "0": {
@ -7016,6 +7046,37 @@
"render": "Hospital" "render": "Hospital"
} }
}, },
"hut": {
"description": "Layer showing basic huts, wilderness huts and alpine huts",
"name": "Huts",
"presets": {
"0": {
"description": "An unserviced fully enclosed hut (with roof and walls) with beds or suitable sleeping areas and a fireplace or stove for heating and cooking.",
"title": "wilderness hut"
},
"1": {
"description": "A serviced remote building located in the mountains intended to provide board and lodging.",
"title": "alpine hut"
},
"2": {
"description": "An unserviced fully enclosed hut (with roof and walls) with beds or suitable sleeping areas <b>without</b> a fireplace or stove.",
"title": "basic hut"
}
},
"tagRenderings": {
"drinking_water": {
"mappings": {
"0": {
"then": "Here is drinking water available."
},
"1": {
"then": "Here is no drinking water available."
}
},
"question": "Is drinking water available here?"
}
}
},
"hydrant": { "hydrant": {
"description": "Map layer to show fire hydrants.", "description": "Map layer to show fire hydrants.",
"name": "Hydrants", "name": "Hydrants",
@ -8626,10 +8687,34 @@
"description": "Layer showing individual parking spaces.", "description": "Layer showing individual parking spaces.",
"name": "Parking Spaces", "name": "Parking Spaces",
"tagRenderings": { "tagRenderings": {
"access": {
"mappings": {
"0": {
"then": "Anyone can use this parking space."
},
"1": {
"then": "Anyone can use this parking space."
},
"2": {
"then": "This parking space is reserved for customers."
},
"3": {
"then": "This parking space is private and cannot be used by the general public."
},
"4": {
"then": "This parking space is reserved for permit holders."
}
},
"question": "Who can use this parking space?",
"render": "Access of parking space: {access}"
},
"capacity": { "capacity": {
"mappings": { "mappings": {
"0": { "0": {
"then": "This parking space has 1 space." "then": "This parking space has 1 space."
},
"1": {
"then": "This parking space has 1 space."
} }
}, },
"render": "This parking spaces has {capacity} spaces." "render": "This parking spaces has {capacity} spaces."
@ -8654,6 +8739,9 @@
"13": { "13": {
"then": "This is a parking space reserved for car sharing." "then": "This is a parking space reserved for car sharing."
}, },
"14": {
"then": "This is a parking space reserved for women."
},
"2": { "2": {
"then": "This is a disabled parking space." "then": "This is a disabled parking space."
}, },
@ -8785,6 +8873,130 @@
"render": "Physiotherapist {name}" "render": "Physiotherapist {name}"
} }
}, },
"picnic_site": {
"description": "Picnic sites for eating outdoors, featuring amenities like toilets, water taps, BBQ, benches and shelters",
"filter": {
"1": {
"options": {
"0": {
"question": "With a firepit"
}
}
},
"2": {
"options": {
"0": {
"question": "With a BBQ"
}
}
},
"3": {
"options": {
"0": {
"question": "With drinking water"
}
}
}
},
"name": "Picnic sites",
"presets": {
"0": {
"description": "A picnic site for eating outdoors, featuring amenities like toilets, water taps, BBQ, benches and shelters",
"title": "a picnic site"
}
},
"tagRenderings": {
"bbq": {
"mappings": {
"0": {
"then": "This picnic site has a BBQ."
},
"1": {
"then": "This picnic site does not have a BBQ."
},
"2": {
"then": "This picnic site has a BBQ, but it is mapped as a different icon."
}
},
"question": "Does this picnic site have a BBQ?"
},
"covered": {
"mappings": {
"0": {
"then": "This picnic site is covered."
},
"1": {
"then": "This picnic site is not covered."
}
},
"question": "Is this picnic site covered?"
},
"drinking_water": {
"mappings": {
"0": {
"then": "This picnic site has drinking water."
},
"1": {
"then": "This picnic site does not have drinking water."
},
"2": {
"then": "This picnic site has drinking water, but it is mapped as a different icon."
}
},
"question": "Does this picnic site have drinking water?"
},
"fireplace": {
"mappings": {
"0": {
"then": "This picnic site has a firepit."
},
"1": {
"then": "This picnic site does not have a firepit."
},
"2": {
"then": "This picnic site has a firepit, but it is mapped as a different icon."
}
},
"question": "Does this picnic site have a firepit?"
},
"name": {
"override": {
"render": "This picnic site is called {name}"
}
},
"openfire": {
"mappings": {
"0": {
"then": "Open fire is allowed at this picnic site."
},
"1": {
"then": "Open fire is not allowed at this picnic site."
},
"2": {
"then": "Open fire is allowed at this picnic site with a permit."
}
},
"question": "Is open fire allowed at this picnic site?"
},
"shelter": {
"mappings": {
"0": {
"then": "This picnic site has a shelter."
},
"1": {
"then": "This picnic site does not have a shelter."
},
"2": {
"then": "This picnic site has a shelter, but is is mapped as a different icon."
}
},
"question": "Does this picnic site have a shelter?"
}
},
"title": {
"render": "Picnic site"
}
},
"picnic_table": { "picnic_table": {
"description": "The layer showing picnic tables", "description": "The layer showing picnic tables",
"name": "Picnic tables", "name": "Picnic tables",
@ -12543,6 +12755,9 @@
"then": "A panning camera" "then": "A panning camera"
}, },
"3": { "3": {
"then": "A 360° camera"
},
"4": {
"then": "A doorbell which might be turned on remotely at any time or by motion detection. These are typically <i>Smart</i>, internet-connected doorbells. Typical brands are Ring, Google Nest, Eufy, …" "then": "A doorbell which might be turned on remotely at any time or by motion detection. These are typically <i>Smart</i>, internet-connected doorbells. Typical brands are Ring, Google Nest, Eufy, …"
} }
}, },
@ -13588,13 +13803,6 @@
"transit_stops": { "transit_stops": {
"description": "Layer showing different types of transit stops.", "description": "Layer showing different types of transit stops.",
"filter": { "filter": {
"0": {
"options": {
"0": {
"question": "With a shelter"
}
}
},
"1": { "1": {
"options": { "options": {
"0": { "0": {

View file

@ -2270,17 +2270,6 @@
}, },
"question": "¿Este lugar tiene un punto de vaciado de aguas grises?" "question": "¿Este lugar tiene un punto de vaciado de aguas grises?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Este lugar tiene baños"
},
"1": {
"then": "Este lugar no tiene baños"
}
},
"question": "¿Este lugar tiene baños?"
},
"caravansites-website": { "caravansites-website": {
"question": "¿Este lugar tiene una página web?", "question": "¿Este lugar tiene una página web?",
"render": "Página web oficial: <a href='{website}'>{website}</a>" "render": "Página web oficial: <a href='{website}'>{website}</a>"
@ -5162,6 +5151,13 @@
} }
} }
}, },
"23": {
"options": {
"0": {
"question": "Con refugio"
}
}
},
"3": { "3": {
"options": { "options": {
"0": { "0": {
@ -7594,6 +7590,9 @@
"mappings": { "mappings": {
"0": { "0": {
"then": "Esta plaza de aparcamiento tiene 1 plaza." "then": "Esta plaza de aparcamiento tiene 1 plaza."
},
"1": {
"then": "Esta plaza de aparcamiento tiene 1 plaza."
} }
}, },
"render": "Esta plaza de aparcamiento tiene {capacity} plazas." "render": "Esta plaza de aparcamiento tiene {capacity} plazas."
@ -10582,6 +10581,9 @@
}, },
"2": { "2": {
"then": "Una cámara panorámica" "then": "Una cámara panorámica"
},
"3": {
"then": "Una cámara de 360°"
} }
}, },
"question": "¿Qué tipo de cámara es esta?" "question": "¿Qué tipo de cámara es esta?"
@ -11317,13 +11319,6 @@
"transit_stops": { "transit_stops": {
"description": "Capa que muestra diferentes tipos de paradas de transporte.", "description": "Capa que muestra diferentes tipos de paradas de transporte.",
"filter": { "filter": {
"0": {
"options": {
"0": {
"question": "Con refugio"
}
}
},
"1": { "1": {
"options": { "options": {
"0": { "0": {

View file

@ -289,13 +289,6 @@
} }
} }
}, },
"caravansites-toilets": {
"mappings": {
"1": {
"then": "Toki honek ez dauka komunik"
}
}
},
"caravansites-website": { "caravansites-website": {
"question": "Toki honek webgunerik ba al du?" "question": "Toki honek webgunerik ba al du?"
} }

View file

@ -1873,17 +1873,6 @@
}, },
"question": "Ce site possède-til un lieu de vidange ?" "question": "Ce site possède-til un lieu de vidange ?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Ce site a des toilettes"
},
"1": {
"then": "Ce site na pas de toilettes"
}
},
"question": "Y-a-til des toilettes sur le site ?"
},
"caravansites-website": { "caravansites-website": {
"question": "Ce lieu a-til un site internet ?", "question": "Ce lieu a-til un site internet ?",
"render": "Site officiel : <a href='{website}'>{website}</a>" "render": "Site officiel : <a href='{website}'>{website}</a>"
@ -3609,6 +3598,13 @@
} }
} }
}, },
"23": {
"options": {
"0": {
"question": "Avec un abri"
}
}
},
"6": { "6": {
"options": { "options": {
"0": { "0": {
@ -6519,6 +6515,9 @@
}, },
"2": { "2": {
"then": "Une caméra panoramique" "then": "Une caméra panoramique"
},
"3": {
"then": "Une caméra 360°"
} }
}, },
"question": "Quel genre de caméra est-ce ?" "question": "Quel genre de caméra est-ce ?"
@ -6543,7 +6542,7 @@
"then": "Une zone intérieure privée est surveillée, par exemple un magasin, un parking souterrain privé…" "then": "Une zone intérieure privée est surveillée, par exemple un magasin, un parking souterrain privé…"
} }
}, },
"question": "De quel genre de surveillance cette caméraest-elle ?" "question": "De quel genre de surveillance cette caméra est-elle ?"
}, },
"Surveillance:zone": { "Surveillance:zone": {
"mappings": { "mappings": {
@ -6916,13 +6915,6 @@
}, },
"transit_stops": { "transit_stops": {
"filter": { "filter": {
"0": {
"options": {
"0": {
"question": "Avec un abri"
}
}
},
"1": { "1": {
"options": { "options": {
"0": { "0": {

View file

@ -466,19 +466,6 @@
"description": "Új hivatalos lakóautóhely hozzáadása. Ez arra vannak kijelölve, hogy lakóautóval ott éjszakázzunk. Lehet, hogy úgy néz ki, mint egy igazi kemping, de az is lehet, hogy csak olyan, mint egy parkoló. Előfordulhat, hogy egyáltalán nem jelzik őket, hanem csak egy önkormányzati határozatban vannak kijelölve. A lakóautósoknak szánt olyan hagyományos parkolók, ahol nem várhatóan nem fognak éjszakázni, -nem minősül- lakóautóhelynek. ", "description": "Új hivatalos lakóautóhely hozzáadása. Ez arra vannak kijelölve, hogy lakóautóval ott éjszakázzunk. Lehet, hogy úgy néz ki, mint egy igazi kemping, de az is lehet, hogy csak olyan, mint egy parkoló. Előfordulhat, hogy egyáltalán nem jelzik őket, hanem csak egy önkormányzati határozatban vannak kijelölve. A lakóautósoknak szánt olyan hagyományos parkolók, ahol nem várhatóan nem fognak éjszakázni, -nem minősül- lakóautóhelynek. ",
"title": "lakóautós megállóhely" "title": "lakóautós megállóhely"
} }
},
"tagRenderings": {
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Itt van WC"
},
"1": {
"then": "Itt nincs WC"
}
},
"question": "Van-e itt WC?"
}
} }
}, },
"charging_station": { "charging_station": {

View file

@ -196,16 +196,6 @@
}, },
"question": "Apakah tempat ini memiliki tempat pembuangan sanitasi?" "question": "Apakah tempat ini memiliki tempat pembuangan sanitasi?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Tempat sini ada tandas"
},
"1": {
"then": "Tempat sini tiada tandas"
}
}
},
"caravansites-website": { "caravansites-website": {
"question": "Tempat sini terada situs web?", "question": "Tempat sini terada situs web?",
"render": "Situs resmi: <a href='{website}'>{website}</a>" "render": "Situs resmi: <a href='{website}'>{website}</a>"

View file

@ -2632,17 +2632,6 @@
}, },
"question": "Questo posto ha una stazione di scarico sanitario?" "question": "Questo posto ha una stazione di scarico sanitario?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Questo posto ha servizi igienici"
},
"1": {
"then": "Questo posto non ha servizi igienici"
}
},
"question": "Questo posto ha servizi igienici?"
},
"caravansites-website": { "caravansites-website": {
"question": "Questo posto ha un sito web?", "question": "Questo posto ha un sito web?",
"render": "Sito web ufficiale: <a href='{website}'>{website}</a>" "render": "Sito web ufficiale: <a href='{website}'>{website}</a>"
@ -5758,6 +5747,13 @@
} }
} }
}, },
"23": {
"options": {
"0": {
"question": "Con una pensilina"
}
}
},
"3": { "3": {
"options": { "options": {
"0": { "0": {
@ -8459,6 +8455,9 @@
"mappings": { "mappings": {
"0": { "0": {
"then": "Questo posto auto ha 1 spazio." "then": "Questo posto auto ha 1 spazio."
},
"1": {
"then": "Questo posto auto ha 1 spazio."
} }
}, },
"render": "Questo posto auto ha {capacity} spazi." "render": "Questo posto auto ha {capacity} spazi."
@ -12109,7 +12108,7 @@
"2": { "2": {
"then": "Una telecamera panoramica" "then": "Una telecamera panoramica"
}, },
"3": { "4": {
"then": "Un campanello che potrebbe essere acceso da remoto in qualsiasi momento o tramite rilevamento del movimento. Questi sono tipicamente campanelli <i>Smart</i>, connessi a Internet. Marchi tipici sono Ring, Google Nest, Eufy, ..." "then": "Un campanello che potrebbe essere acceso da remoto in qualsiasi momento o tramite rilevamento del movimento. Questi sono tipicamente campanelli <i>Smart</i>, connessi a Internet. Marchi tipici sono Ring, Google Nest, Eufy, ..."
} }
}, },
@ -13124,13 +13123,6 @@
"transit_stops": { "transit_stops": {
"description": "Livello che mostra diversi tipi di fermate dei mezzi pubblici.", "description": "Livello che mostra diversi tipi di fermate dei mezzi pubblici.",
"filter": { "filter": {
"0": {
"options": {
"0": {
"question": "Con una pensilina"
}
}
},
"1": { "1": {
"options": { "options": {
"0": { "0": {

View file

@ -217,17 +217,6 @@
}, },
"question": "この場所に衛生的なゴミ捨て場はありますか?" "question": "この場所に衛生的なゴミ捨て場はありますか?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "ここにはトイレがある"
},
"1": {
"then": "ここにはトイレがない"
}
},
"question": "ここにトイレはありますか?"
},
"caravansites-website": { "caravansites-website": {
"question": "ここにはウェブサイトがありますか?", "question": "ここにはウェブサイトがありますか?",
"render": "公式Webサイト: <a href='{website}'>{website}</a>" "render": "公式Webサイト: <a href='{website}'>{website}</a>"

View file

@ -443,17 +443,6 @@
"question": "Hva heter dette stedet?", "question": "Hva heter dette stedet?",
"render": "Dette stedet heter {name}" "render": "Dette stedet heter {name}"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Dette stedet har toalettfasiliteter"
},
"1": {
"then": "Dette stedet har ikke toalettfasiliteter"
}
},
"question": "Har dette stedet toaletter?"
},
"caravansites-website": { "caravansites-website": {
"question": "Har dette stedet en nettside?", "question": "Har dette stedet en nettside?",
"render": "Offisiell nettside: <a href='{website}'>{website}</a>" "render": "Offisiell nettside: <a href='{website}'>{website}</a>"

View file

@ -2546,17 +2546,6 @@
}, },
"question": "Heeft deze plaats een loosplaats?" "question": "Heeft deze plaats een loosplaats?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Deze plaats heeft toiletten"
},
"1": {
"then": "Deze plaats heeft geen toiletten"
}
},
"question": "Heeft deze plaats toiletten?"
},
"caravansites-website": { "caravansites-website": {
"question": "Heeft deze plaats een website?", "question": "Heeft deze plaats een website?",
"render": "Officiële website: : <a href='{website}'>{website}</a>" "render": "Officiële website: : <a href='{website}'>{website}</a>"
@ -7622,10 +7611,34 @@
"description": "Laag met individuele parkeerplekken.", "description": "Laag met individuele parkeerplekken.",
"name": "Parkeerplekken", "name": "Parkeerplekken",
"tagRenderings": { "tagRenderings": {
"access": {
"mappings": {
"0": {
"then": "Iedereen kan deze parkeerplek gebruiken."
},
"1": {
"then": "Iedereen kan deze parkeerplek gebruiken."
},
"2": {
"then": "Deze parkeerplek is gereserveerd voor klanten."
},
"3": {
"then": "Deze parkeerplek is privé en mag niet door het grote publiek worden gebruikt."
},
"4": {
"then": "Deze parkeerplek is gereserveerd voor vergunninghouders."
}
},
"question": "Wie mag deze parkeerplek gebruiken?",
"render": "Toegang tot parkeerplek: {access}"
},
"capacity": { "capacity": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Deze parkeerplek heeft 1 plaats." "then": "Deze parkeerplek heeft 1 plaats."
},
"1": {
"then": "Deze parkeerplek heeft 1 plaats."
} }
}, },
"render": "Deze parkeerplek heeft {capacity} plaatsen." "render": "Deze parkeerplek heeft {capacity} plaatsen."
@ -7650,6 +7663,9 @@
"13": { "13": {
"then": "Deze parkeerplek is gereserveerd voor autodelen." "then": "Deze parkeerplek is gereserveerd voor autodelen."
}, },
"14": {
"then": "Deze parkeerplek is gereserveerd voor vrouwen."
},
"2": { "2": {
"then": "Dit is een gehandicaptenparkeerplaats." "then": "Dit is een gehandicaptenparkeerplaats."
}, },
@ -7780,6 +7796,130 @@
"render": "Kinesist {name}" "render": "Kinesist {name}"
} }
}, },
"picnic_site": {
"description": "Picknickplaatsen voor het eten in de buitenlucht, met voorzieningen zoals toiletten, waterkranen, BBQ, banken en schuilplaatsen",
"filter": {
"1": {
"options": {
"0": {
"question": "Met een vuurplaats"
}
}
},
"2": {
"options": {
"0": {
"question": "Met een BBQ"
}
}
},
"3": {
"options": {
"0": {
"question": "Met drinkwater"
}
}
}
},
"name": "Picknickplaatsen",
"presets": {
"0": {
"description": "Een picknickplaats voor het eten in de buitenlucht, met voorzieningen zoals toiletten, waterkranen, BBQ, banken en schuilplaatsen",
"title": "een picknickplaats"
}
},
"tagRenderings": {
"bbq": {
"mappings": {
"0": {
"then": "Deze picknickplaats heeft een BBQ."
},
"1": {
"then": "Deze picknickplaats heeft geen BBQ."
},
"2": {
"then": "Deze picknickplaats heeft een BBQ, maar deze staat los op de kaart."
}
},
"question": "Heeft deze picknickplaats een BBQ?"
},
"covered": {
"mappings": {
"0": {
"then": "Deze picknickplaats is overdekt."
},
"1": {
"then": "Deze picknickplaats is niet overdekt."
}
},
"question": "Is deze picknickplaats overdekt?"
},
"drinking_water": {
"mappings": {
"0": {
"then": "Deze picknickplaats heeft drinkwater."
},
"1": {
"then": "Deze picknickplaats heeft geen drinkwater."
},
"2": {
"then": "Deze picknickplaats heeft drinkwater, maar deze staat los op de kaart."
}
},
"question": "Heeft deze picknickplaats drinkwater?"
},
"fireplace": {
"mappings": {
"0": {
"then": "Deze picknickplaats heeft een vuurplaats."
},
"1": {
"then": "Deze picknickplaats heeft geen vuurplaats."
},
"2": {
"then": "Deze picknickplaats heeft een vuurplaats, maar deze staat los op de kaart."
}
},
"question": "Heeft deze picknickplaats een vuurplaats?"
},
"name": {
"override": {
"render": "Deze picknickplaats heet {name}"
}
},
"openfire": {
"mappings": {
"0": {
"then": "Open vuur is toegestaan op deze picknickplaats."
},
"1": {
"then": "Open vuur is niet toegestaan op deze picknickplaats."
},
"2": {
"then": "Open vuur is toegestaan op deze picknickplaats met een vergunning."
}
},
"question": "Is open vuur toegestaan op deze picknickplaats?"
},
"shelter": {
"mappings": {
"0": {
"then": "Deze picknickplaats heeft een schuilplaats."
},
"1": {
"then": "Deze picknickplaats heeft geen schuilplaats."
},
"2": {
"then": "Deze picknickplaats heeft een schuilplaats, maar deze staat los op de kaart."
}
},
"question": "Heeft deze picknickplaats een schuilplaats?"
}
},
"title": {
"render": "Picknickplaats"
}
},
"picnic_table": { "picnic_table": {
"description": "Deze laag toont picknicktafels", "description": "Deze laag toont picknicktafels",
"name": "Picknicktafels", "name": "Picknicktafels",

View file

@ -1143,17 +1143,6 @@
}, },
"question": "Czy w tym miejscu znajduje się stacja zrzutu ścieków sanitarnych?" "question": "Czy w tym miejscu znajduje się stacja zrzutu ścieków sanitarnych?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "To miejsce ma toalety"
},
"1": {
"then": "To miejsce nie ma toalet"
}
},
"question": "Czy to miejsce ma toalety?"
},
"caravansites-website": { "caravansites-website": {
"question": "Czy to miejsce ma stronę internetową?", "question": "Czy to miejsce ma stronę internetową?",
"render": "Official website: <a href='{website}'>{website}</a>" "render": "Official website: <a href='{website}'>{website}</a>"

View file

@ -1418,17 +1418,6 @@
}, },
"question": "Este local tem uma estação de aterro sanitário?" "question": "Este local tem uma estação de aterro sanitário?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Este lugar tem casa de banho"
},
"1": {
"then": "Este lugar não tem casas de banho"
}
},
"question": "Este lugar tem casas de banho?"
},
"caravansites-website": { "caravansites-website": {
"question": "Este lugar tem um website?", "question": "Este lugar tem um website?",
"render": "Site oficial: <a href='{website}'>{website}</a>" "render": "Site oficial: <a href='{website}'>{website}</a>"

View file

@ -1428,17 +1428,6 @@
}, },
"question": "Este local tem uma estação de aterro sanitário?" "question": "Este local tem uma estação de aterro sanitário?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Este lugar tem banheiros"
},
"1": {
"then": "Este lugar não tem banheiros"
}
},
"question": "Este lugar tem banheiros?"
},
"caravansites-website": { "caravansites-website": {
"question": "Este lugar tem um website?", "question": "Este lugar tem um website?",
"render": "Site oficial: <a href='{website}'>{website}</a>" "render": "Site oficial: <a href='{website}'>{website}</a>"

View file

@ -710,17 +710,6 @@
}, },
"question": "В этом кемпинге есть место для слива отходов из туалетных резервуаров?" "question": "В этом кемпинге есть место для слива отходов из туалетных резервуаров?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "В этом месте есть туалеты"
},
"1": {
"then": "В этом месте нет туалетов"
}
},
"question": "Здесь есть туалеты?"
},
"caravansites-website": { "caravansites-website": {
"question": "Есть ли у этого места веб-сайт?", "question": "Есть ли у этого места веб-сайт?",
"render": "Официальный сайт: <a href='{website}'>{website}</a>" "render": "Официальный сайт: <a href='{website}'>{website}</a>"

View file

@ -594,17 +594,6 @@
}, },
"question": "這個地方有衛生設施嗎?" "question": "這個地方有衛生設施嗎?"
}, },
"caravansites-toilets": {
"mappings": {
"0": {
"then": "這個地方有廁所"
},
"1": {
"then": "這個地方並沒有廁所"
}
},
"question": "這個地方有廁所嗎?"
},
"caravansites-website": { "caravansites-website": {
"question": "這個地方有網站嗎?", "question": "這個地方有網站嗎?",
"render": "官方網站:<a href='{website}'>{website}</a>" "render": "官方網站:<a href='{website}'>{website}</a>"

View file

@ -8,6 +8,10 @@
"description": "On this map, one can find and mark nearby defibrillators", "description": "On this map, one can find and mark nearby defibrillators",
"title": "Defibrillators" "title": "Defibrillators"
}, },
"arcade": {
"description": "A map of arcades",
"title": "Arcades"
},
"architecture": { "architecture": {
"description": "A map showing the architectural style of buildings", "description": "A map showing the architectural style of buildings",
"title": "Buildings with an architectural style" "title": "Buildings with an architectural style"

View file

@ -5444,6 +5444,47 @@ button.unstyled, .button-unstyled button {
padding: 0; padding: 0;
} }
/****** Tablist elements *****/
.tablist {
margin: 0.25rem;
padding: 0.5rem;
border: 2px dashed var(--button-background-hover);
border-radius: 0.5rem;
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.tab {
border: unset;
border-radius: 0;
transition: all;
color: var(--foreground-color);
border-bottom: 2px solid var(--foreground-color);
font-weight: bold;
margin: 0.25rem;
padding: 0.25rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.tab-selected {
opacity: 100%;
background: var(--interactive-background);
}
/* Actually used, don't remove*/
.tab-unselected {
background: #00000000 !important;
opacity: 60%;
}
.tab-unselected:hover {
background: var(--interactive-background);
}
/******* Other input elements ******/ /******* Other input elements ******/
.hover-alert:hover { .hover-alert:hover {

View file

@ -10,6 +10,7 @@ import { Changes } from "../Osm/Changes"
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig" import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import { WithChangesState } from "../../Models/ThemeViewState/WithChangesState" import { WithChangesState } from "../../Models/ThemeViewState/WithChangesState"
import Objects from "../../Utils/Objects"
export default class SelectedElementTagsUpdater { export default class SelectedElementTagsUpdater {
private static readonly metatags = new Set([ private static readonly metatags = new Set([
@ -160,7 +161,7 @@ export default class SelectedElementTagsUpdater {
const newGeometry = osmObject.asGeoJson()?.geometry const newGeometry = osmObject.asGeoJson()?.geometry
const oldFeature = state.indexedFeatures.featuresById.data.get(id) const oldFeature = state.indexedFeatures.featuresById.data.get(id)
const oldGeometry = oldFeature?.geometry const oldGeometry = oldFeature?.geometry
if (oldGeometry !== undefined && !Utils.SameObject(newGeometry, oldGeometry)) { if (oldGeometry !== undefined && !Objects.sameObject(newGeometry, oldGeometry)) {
console.log("Detected a difference in geometry for ", id) console.log("Detected a difference in geometry for ", id)
this.invalidateCache(s) this.invalidateCache(s)
oldFeature.geometry = newGeometry oldFeature.geometry = newGeometry

View file

@ -4,9 +4,9 @@ import TileLocalStorage from "./TileLocalStorage"
import { GeoOperations } from "../../GeoOperations" import { GeoOperations } from "../../GeoOperations"
import FeaturePropertiesStore from "./FeaturePropertiesStore" import FeaturePropertiesStore from "./FeaturePropertiesStore"
import { UIEventSource } from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
import { Utils } from "../../../Utils"
import { Tiles } from "../../../Models/TileRange" import { Tiles } from "../../../Models/TileRange"
import { BBox } from "../../BBox" import { BBox } from "../../BBox"
import { Lists } from "../../../Utils/Lists"
class SingleTileSaver { class SingleTileSaver {
private readonly _storage: UIEventSource<Feature[]> private readonly _storage: UIEventSource<Feature[]>
@ -31,7 +31,7 @@ class SingleTileSaver {
} }
public saveFeatures(features: Feature[]) { public saveFeatures(features: Feature[]) {
if (Utils.sameList(features, this._storage.data)) { if (Lists.sameList(features, this._storage.data)) {
return return
} }
for (const feature of features) { for (const feature of features) {

View file

@ -6,7 +6,7 @@ import { Stores, UIEventSource } from "../../UIEventSource"
import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" import { FeatureSource, IndexedFeatureSource } from "../FeatureSource"
import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription" import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription"
import { Feature } from "geojson" import { Feature } from "geojson"
import { Utils } from "../../../Utils" import Objects from "../../../Utils/Objects"
export default class ChangeGeometryApplicator implements FeatureSource { export default class ChangeGeometryApplicator implements FeatureSource {
public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([]) public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
@ -69,7 +69,7 @@ export default class ChangeGeometryApplicator implements FeatureSource {
// We only apply the last change as that one'll have the latest geometry // We only apply the last change as that one'll have the latest geometry
const change = changesForFeature[changesForFeature.length - 1] const change = changesForFeature[changesForFeature.length - 1]
copy.geometry = ChangeDescriptionTools.getGeojsonGeometry(change) copy.geometry = ChangeDescriptionTools.getGeojsonGeometry(change)
if (Utils.SameObject(copy.geometry, feature.geometry)) { if (Objects.sameObject(copy.geometry, feature.geometry)) {
// No actual changes: pass along the original // No actual changes: pass along the original
newFeatures.push(feature) newFeatures.push(feature)
continue continue

View file

@ -9,7 +9,7 @@ import { BBox } from "../../BBox"
import { OsmFeature } from "../../../Models/OsmFeature" import { OsmFeature } from "../../../Models/OsmFeature"
import { Lists } from "../../../Utils/Lists" import { Lists } from "../../../Utils/Lists"
;("use strict") ("use strict")
/** /**
* A wrapper around the 'Overpass'-object. * A wrapper around the 'Overpass'-object.
@ -229,7 +229,7 @@ export default class OverpassFeatureSource<T extends OsmFeature = OsmFeature> im
const requestedBounds = this.state.bounds.data const requestedBounds = this.state.bounds.data
if ( if (
this._lastQueryBBox !== undefined && this._lastQueryBBox !== undefined &&
Utils.sameList(this._layersToDownload.data, this._lastRequestedLayers) && Lists.sameList(this._layersToDownload.data, this._lastRequestedLayers) &&
requestedBounds.isContainedIn(this._lastQueryBBox) requestedBounds.isContainedIn(this._lastQueryBBox)
) { ) {
return undefined return undefined

View file

@ -32,7 +32,7 @@ export default class FilterSearch {
.split(" ") .split(" ")
.map((query) => { .map((query) => {
if (!Strings.isEmoji(query)) { if (!Strings.isEmoji(query)) {
return Utils.simplifyStringForSearch(query) return Strings.simplifyStringForSearch(query)
} }
return query return query
}) })
@ -64,7 +64,7 @@ export default class FilterSearch {
option.searchTerms?.["en"] ?? option.searchTerms?.["en"] ??
[]), []),
].flatMap((term) => [term, ...(term?.split(" ") ?? [])]) ].flatMap((term) => [term, ...(term?.split(" ") ?? [])])
terms = terms.map((t) => Utils.simplifyStringForSearch(t)) terms = terms.map((t) => Strings.simplifyStringForSearch(t))
terms.push(option.emoji) terms.push(option.emoji)
Lists.noNullInplace(terms) Lists.noNullInplace(terms)
const distances = queries.flatMap((query) => const distances = queries.flatMap((query) =>

View file

@ -2,7 +2,7 @@ import SearchUtils from "./SearchUtils"
import ThemeSearch from "./ThemeSearch" import ThemeSearch from "./ThemeSearch"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig" import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
import { Utils } from "../../Utils" import { Strings } from "../../Utils/Strings"
export default class LayerSearch { export default class LayerSearch {
private readonly _theme: ThemeConfig private readonly _theme: ThemeConfig
@ -24,7 +24,7 @@ export default class LayerSearch {
const queryParts = query const queryParts = query
.trim() .trim()
.split(" ") .split(" ")
.map((q) => Utils.simplifyStringForSearch(q)) .map((q) => Strings.simplifyStringForSearch(q))
for (const id in ThemeSearch.officialThemes.layers) { for (const id in ThemeSearch.officialThemes.layers) {
if (options?.whitelist && !options?.whitelist.has(id)) { if (options?.whitelist && !options?.whitelist.has(id)) {
continue continue

View file

@ -6,6 +6,7 @@ import { GeoOperations } from "../GeoOperations"
import { ImmutableStore, Store, Stores } from "../UIEventSource" import { ImmutableStore, Store, Stores } from "../UIEventSource"
import OpenStreetMapIdSearch from "./OpenStreetMapIdSearch" import OpenStreetMapIdSearch from "./OpenStreetMapIdSearch"
import { Lists } from "../../Utils/Lists" import { Lists } from "../../Utils/Lists"
import { Strings } from "../../Utils/Strings"
type IntermediateResult = { type IntermediateResult = {
feature: Feature feature: Feature
@ -61,7 +62,7 @@ export default class LocalElementSearch implements GeocodingProvider {
...searchTerms ...searchTerms
.flatMap((entry) => entry.split(/ /)) .flatMap((entry) => entry.split(/ /))
.map((entry) => { .map((entry) => {
let simplified = Utils.simplifyStringForSearch(entry) let simplified = Strings.simplifyStringForSearch(entry)
if (matchStart) { if (matchStart) {
simplified = simplified.slice(0, query.length) simplified = simplified.slice(0, query.length)
} }
@ -103,7 +104,7 @@ export default class LocalElementSearch implements GeocodingProvider {
const centerPoint: [number, number] = [center.lon, center.lat] const centerPoint: [number, number] = [center.lon, center.lat]
const properties = this._state.perLayer const properties = this._state.perLayer
const candidateId = OpenStreetMapIdSearch.extractId(query) const candidateId = OpenStreetMapIdSearch.extractId(query)
query = Utils.simplifyStringForSearch(query) query = Strings.simplifyStringForSearch(query)
const partials: Store<IntermediateResult[]>[] = [] const partials: Store<IntermediateResult[]>[] = []

View file

@ -2,6 +2,7 @@ import Locale from "../../UI/i18n/Locale"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import ThemeSearch from "./ThemeSearch" import ThemeSearch from "./ThemeSearch"
import { Lists } from "../../Utils/Lists" import { Lists } from "../../Utils/Lists"
import { Strings } from "../../Utils/Strings"
export default class SearchUtils { export default class SearchUtils {
/** Applies special search terms, such as 'studio', 'osmcha', ... /** Applies special search terms, such as 'studio', 'osmcha', ...
@ -60,7 +61,7 @@ export default class SearchUtils {
const queryParts = query const queryParts = query
.trim() .trim()
.split(" ") .split(" ")
.map((q) => Utils.simplifyStringForSearch(q)) .map((q) => Strings.simplifyStringForSearch(q))
let terms: string[] let terms: string[]
if (Array.isArray(keywords)) { if (Array.isArray(keywords)) {
terms = keywords terms = keywords
@ -74,7 +75,7 @@ export default class SearchUtils {
const q = queryParts[i] const q = queryParts[i]
let minDistance: number = 99 let minDistance: number = 99
for (const term of termsAll) { for (const term of termsAll) {
const d = Utils.levenshteinDistance(q, Utils.simplifyStringForSearch(term)) const d = Utils.levenshteinDistance(q, Strings.simplifyStringForSearch(term))
if (d < minDistance) { if (d < minDistance) {
minDistance = d minDistance = d
} }

View file

@ -314,6 +314,14 @@ export class And<T extends TagsFilter = TagsFilter> extends TagsFilter {
for (let j = i + 1; j < optimized.length; j++) { for (let j = i + 1; j < optimized.length; j++) {
const ti = optimized[i] const ti = optimized[i]
const tj = optimized[j] const tj = optimized[j]
if (
!ti ||
!tj ||
typeof ti.shadows !== "function" ||
typeof tj.shadows !== "function"
) {
continue
}
if (ti.shadows(tj)) { if (ti.shadows(tj)) {
// if 'ti' is true, this implies 'tj' is always true as well. // if 'ti' is true, this implies 'tj' is always true as well.
// if 'ti' is false, then 'tj' might be true or false // if 'ti' is false, then 'tj' might be true or false
@ -322,6 +330,7 @@ export class And<T extends TagsFilter = TagsFilter> extends TagsFilter {
// If 'ti' is true, then 'tj' will be true too and 'tj' can be ignored // If 'ti' is true, then 'tj' will be true too and 'tj' can be ignored
// If 'ti' is false, then the entire expression will be false and it doesn't matter what 'tj' yields // If 'ti' is false, then the entire expression will be false and it doesn't matter what 'tj' yields
optimized.splice(j, 1) optimized.splice(j, 1)
j--
} else if (tj.shadows(ti)) { } else if (tj.shadows(ti)) {
optimized.splice(i, 1) optimized.splice(i, 1)
i-- i--

View file

@ -120,7 +120,7 @@ export default class SubstitutingTag extends TagsFilter {
} }
isNegative(): boolean { isNegative(): boolean {
return false return this._value === ""
} }
visit(f: (tagsFilter: TagsFilter) => void) { visit(f: (tagsFilter: TagsFilter) => void) {

View file

@ -994,11 +994,31 @@ export class TagUtils {
].join("\n") ].join("\n")
} }
static fromProperties(tags: Record<string, string>): TagConfigJson | boolean { public static fromProperties(tags: Record<string, string>): TagConfigJson | boolean {
const opt = new And(Object.keys(tags).map((k) => new Tag(k, tags[k]))).optimize() const opt = new And(Object.keys(tags).map((k) => new Tag(k, tags[k]))).optimize()
if (opt === true || opt === false) { if (opt === true || opt === false) {
return opt return opt
} }
return opt.asJson() return opt.asJson()
} }
/**
* Returns a similarly structured tag, but all tags with an empty value are removed.
* Those are assumed to be all met (and thus true)
*
* new And([new Tag("a", "b"), new Tag("c", "")] // => new Tag("a","b")
* new And([new Tag("c", "")] // => true
*/
public static removeEmptyParts(tag: UploadableTag): UploadableTag | true {
if (tag["and"]) {
const tags = <UploadableTag[]>tag["and"]
const cleaned = tags.map(t => TagUtils.removeEmptyParts(t))
const filtered = <UploadableTag[]>cleaned.filter(t => t !== true)
return new And(filtered)
}
if (tag.isNegative()) {
return true
}
return tag
}
} }

View file

@ -1,5 +1,6 @@
import { Utils } from "../Utils" import { Utils } from "../Utils"
import { Readable, Subscriber, Unsubscriber, Updater, Writable } from "svelte/store" import { Readable, Subscriber, Unsubscriber, Updater, Writable } from "svelte/store"
import { Lists } from "../Utils/Lists"
/** /**
* Various static utils * Various static utils
@ -66,7 +67,7 @@ export class Stores {
stable.setData(undefined) stable.setData(undefined)
return return
} }
if (Utils.sameList(stable.data, list)) { if (Lists.sameList(stable.data, list)) {
return return
} }
stable.setData(list) stable.setData(list)

View file

@ -25,6 +25,7 @@ import { eliCategory } from "../../RasterLayerProperties"
import licenses from "../../../assets/generated/license_info.json" import licenses from "../../../assets/generated/license_info.json"
import { Strings } from "../../../Utils/Strings" import { Strings } from "../../../Utils/Strings"
import { Lists } from "../../../Utils/Lists" import { Lists } from "../../../Utils/Lists"
import Objects from "../../../Utils/Objects"
export class ValidateLanguageCompleteness extends DesugaringStep<ThemeConfig> { export class ValidateLanguageCompleteness extends DesugaringStep<ThemeConfig> {
private readonly _languages: string[] private readonly _languages: string[]
@ -1085,11 +1086,8 @@ export class DetectDuplicatePresets extends DesugaringStep<ThemeConfig> {
const presetBTags = optimizedTags[j] const presetBTags = optimizedTags[j]
const presetB = presets[j] const presetB = presets[j]
if ( if (
Utils.SameObject(presetATags, presetBTags) && Objects.sameObject(presetATags, presetBTags) &&
Utils.sameList( Lists.sameList(presetA.preciseInput.snapToLayers, presetB.preciseInput.snapToLayers)
presetA.preciseInput.snapToLayers,
presetB.preciseInput.snapToLayers
)
) { ) {
context.err( context.err(
`This theme has multiple presets with the same tags: ${presetATags.asHumanString( `This theme has multiple presets with the same tags: ${presetATags.asHumanString(

View file

@ -43,11 +43,11 @@
} }
}} }}
> >
<div class="interactive sticky top-0 flex items-center justify-between"> <div class="tablist sticky top-0 flex items-center justify-center">
<TabList class="flex flex-wrap"> <TabList class="flex items-center justify-center">
{#if $$slots.title0} {#if $$slots.title0}
<Tab <Tab
class={({ selected }) => twJoin("tab", selected && "primary", !$condition0 && "hidden")} class={({ selected }) => twJoin("tab", selected ? "tab-selected": "tab-unselected", !$condition0 && "hidden")}
> >
<div bind:this={tabElements[0]} class="flex"> <div bind:this={tabElements[0]} class="flex">
<slot name="title0">Tab 0</slot> <slot name="title0">Tab 0</slot>
@ -56,7 +56,7 @@
{/if} {/if}
{#if $$slots.title1} {#if $$slots.title1}
<Tab <Tab
class={({ selected }) => twJoin("tab", selected && "primary", !$condition1 && "hidden")} class={({ selected }) => twJoin("tab", selected ? "tab-selected": "tab-unselected", !$condition1 && "hidden")}
> >
<div bind:this={tabElements[1]} class="flex"> <div bind:this={tabElements[1]} class="flex">
<slot name="title1" /> <slot name="title1" />
@ -65,7 +65,7 @@
{/if} {/if}
{#if $$slots.title2} {#if $$slots.title2}
<Tab <Tab
class={({ selected }) => twJoin("tab", selected && "primary", !$condition2 && "hidden")} class={({ selected }) => twJoin("tab", selected ? "tab-selected": "tab-unselected", !$condition2 && "hidden")}
> >
<div bind:this={tabElements[2]} class="flex"> <div bind:this={tabElements[2]} class="flex">
<slot name="title2" /> <slot name="title2" />
@ -74,7 +74,7 @@
{/if} {/if}
{#if $$slots.title3} {#if $$slots.title3}
<Tab <Tab
class={({ selected }) => twJoin("tab", selected && "primary", !$condition3 && "hidden")} class={({ selected }) => twJoin("tab", selected ? "tab-selected": "tab-unselected", !$condition3 && "hidden")}
> >
<div bind:this={tabElements[3]} class="flex"> <div bind:this={tabElements[3]} class="flex">
<slot name="title3" /> <slot name="title3" />
@ -83,7 +83,7 @@
{/if} {/if}
{#if $$slots.title4} {#if $$slots.title4}
<Tab <Tab
class={({ selected }) => twJoin("tab", selected && "primary", !$condition4 && "hidden")} class={({ selected }) => twJoin("tab", selected ? "tab-selected": "tab-unselected", !$condition4 && "hidden")}
> >
<div bind:this={tabElements[4]} class="flex"> <div bind:this={tabElements[4]} class="flex">
<slot name="title4" /> <slot name="title4" />
@ -92,7 +92,7 @@
{/if} {/if}
{#if $$slots.title5} {#if $$slots.title5}
<Tab <Tab
class={({ selected }) => twJoin("tab", selected && "primary", !$condition5 && "hidden")} class={({ selected }) => twJoin("tab", selected ? "tab-selected": "tab-unselected", !$condition5 && "hidden")}
> >
<div bind:this={tabElements[5]} class="flex"> <div bind:this={tabElements[5]} class="flex">
<slot name="title5" /> <slot name="title5" />
@ -101,7 +101,7 @@
{/if} {/if}
{#if $$slots.title6} {#if $$slots.title6}
<Tab <Tab
class={({ selected }) => twJoin("tab", selected && "primary", !$condition6 && "hidden")} class={({ selected }) => twJoin("tab", selected ? "tab-selected": "tab-unselected", !$condition6 && "hidden")}
> >
<div bind:this={tabElements[6]} class="flex"> <div bind:this={tabElements[6]} class="flex">
<slot name="title6" /> <slot name="title6" />
@ -167,19 +167,6 @@
height: calc(100% - 2rem); height: calc(100% - 2rem);
} }
:global(.tab) {
margin: 0.25rem;
padding: 0.25rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
border-radius: 1rem;
}
:global(.tab .flex) {
align-items: center;
gap: 0.25rem;
}
:global(.tab span|div) { :global(.tab span|div) {
align-items: center; align-items: center;
gap: 0.25rem; gap: 0.25rem;
@ -190,8 +177,4 @@
fill: var(--interactive-contrast); fill: var(--interactive-contrast);
} }
:global(.tab-unselected) {
background-color: var(--background-color) !important;
color: var(--foreground-color) !important;
}
</style> </style>

View file

@ -55,15 +55,13 @@
import ImageUploadQueue from "../../Logic/ImageProviders/ImageUploadQueue" import ImageUploadQueue from "../../Logic/ImageProviders/ImageUploadQueue"
import QueuedImagesView from "../Image/QueuedImagesView.svelte" import QueuedImagesView from "../Image/QueuedImagesView.svelte"
import InsetSpacer from "../Base/InsetSpacer.svelte" import InsetSpacer from "../Base/InsetSpacer.svelte"
import UserCircle from "@rgossiaux/svelte-heroicons/solid/UserCircle"
import OfflineManagement from "./OfflineManagement.svelte" import OfflineManagement from "./OfflineManagement.svelte"
import { GlobeEuropeAfrica } from "@babeard/svelte-heroicons/solid/GlobeEuropeAfrica" import { GlobeEuropeAfrica } from "@babeard/svelte-heroicons/solid/GlobeEuropeAfrica"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import Avatar from "../Base/Avatar.svelte" import Avatar from "../Base/Avatar.svelte"
import { SpecialVisualizationSvelte } from "../SpecialVisualization"
import ThemeViewState from "../../Models/ThemeViewState"
import { Changes } from "../../Logic/Osm/Changes" import { Changes } from "../../Logic/Osm/Changes"
import PendingChangesView from "./PendingChangesView.svelte" import PendingChangesView from "./PendingChangesView.svelte"
import { DevicePhoneMobileIcon } from "@babeard/svelte-heroicons/solid"
export let state: { export let state: {
favourites: FavouritesFeatureSource favourites: FavouritesFeatureSource
@ -236,6 +234,12 @@
<Tr t={Translations.t.general.attribution.emailCreators} /> <Tr t={Translations.t.general.attribution.emailCreators} />
</a> </a>
{#if !$isAndroid}
<a href="https://app.mapcomplete.org">
<DevicePhoneMobileIcon class="h-6 w-6"/>
<Tr t={Translations.t.general.menu.downloadApp}/>
</a>
{/if}
<a class="flex" href="https://en.osm.town/@MapComplete" target="_blank"> <a class="flex" href="https://en.osm.town/@MapComplete" target="_blank">
<Mastodon class="h-6 w-6" /> <Mastodon class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.followOnMastodon} /> <Tr t={Translations.t.general.attribution.followOnMastodon} />

View file

@ -12,9 +12,9 @@
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson" import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
import { Or } from "../../Logic/Tags/Or" import { Or } from "../../Logic/Tags/Or"
import { Utils } from "../../Utils"
import ChartJs from "../Base/ChartJs.svelte" import ChartJs from "../Base/ChartJs.svelte"
import { ChartJsUtils } from "../Base/ChartJsUtils" import { ChartJsUtils } from "../Base/ChartJsUtils"
import { Lists } from "../../Utils/Lists"
export let onlyShowUsername: string[] export let onlyShowUsername: string[]
export let features: Feature[] export let features: Feature[]

View file

@ -33,6 +33,9 @@
import GeocodeResults from "./Search/GeocodeResults.svelte" import GeocodeResults from "./Search/GeocodeResults.svelte"
import MagnifyingGlassCircle from "@babeard/svelte-heroicons/mini/MagnifyingGlassCircle" import MagnifyingGlassCircle from "@babeard/svelte-heroicons/mini/MagnifyingGlassCircle"
import type { GeocodeResult } from "../Logic/Search/GeocodingProvider" import type { GeocodeResult } from "../Logic/Search/GeocodingProvider"
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui"
import WikipediaTitle from "./Wikipedia/WikipediaTitle.svelte"
import WikipediaArticle from "./Wikipedia/WikipediaArticle.svelte"
console.log("Loading inspector GUI") console.log("Loading inspector GUI")
let username = QueryParameters.GetQueryParameter("user", undefined, "Inspect this user") let username = QueryParameters.GetQueryParameter("user", undefined, "Inspect this user")
@ -49,7 +52,7 @@
new CoordinateSearch(), new CoordinateSearch(),
new OpenLocationCodeSearch(), new OpenLocationCodeSearch(),
new PhotonSearch(true, 2), new PhotonSearch(true, 2),
new PhotonSearch() new PhotonSearch(),
) )
let showSearchDrawer = new UIEventSource(true) let showSearchDrawer = new UIEventSource(true)
let searchIsFocussed = new UIEventSource(false) let searchIsFocussed = new UIEventSource(false)
@ -138,7 +141,7 @@
const overpass = new Overpass( const overpass = new Overpass(
Constants.defaultOverpassUrls[0], Constants.defaultOverpassUrls[0],
undefined, undefined,
user.split(";").map((user) => 'nw(user_touched:"' + user + '");') user.split(";").map((user) => "nw(user_touched:\"" + user + "\");"),
) )
if (!maplibremap.bounds.data) { if (!maplibremap.bounds.data) {
return return
@ -161,8 +164,6 @@
return true return true
}) })
let mode: "map" | "table" | "aggregate" | "images" = "map"
let showPreviouslyVisited = new UIEventSource(true) let showPreviouslyVisited = new UIEventSource(true)
const t = Translations.t.inspector const t = Translations.t.inspector
@ -181,8 +182,8 @@
<div class="flex h-screen w-full flex-col"> <div class="flex h-screen w-full flex-col">
<div class="low-interaction flex flex-wrap items-center gap-x-2 p-2"> <div class="low-interaction flex flex-wrap items-center gap-x-2 p-2">
<MagnifyingGlassCircle class="h-12 w-12" />
<h1 class="m-0 mx-2 flex-shrink-0"> <h1 class="m-0 mx-2 flex-shrink-0">
<MagnifyingGlassCircle class="h-6 w-6" />
<Tr t={t.title} /> <Tr t={t.title} />
</h1> </h1>
<ValidatedInput <ValidatedInput
@ -207,89 +208,98 @@
</a> </a>
</div> </div>
<div class="flex"> <TabGroup class="flex-grow flex flex-col">
<button class:primary={mode === "map"} on:click={() => (mode = "map")}> <TabList class="tablist">
<Tr t={t.mapView} /> <Tab class={({ selected }) => ("tab "+ (selected ? "tab-selected" : "tab-unselected"))}>
</button>
<button class:primary={mode === "table"} on:click={() => (mode = "table")}>
<Tr t={t.tableView} />
</button>
<button class:primary={mode === "aggregate"} on:click={() => (mode = "aggregate")}>
<Tr t={t.aggregateView} />
</button>
<button class:primary={mode === "images"} on:click={() => (mode = "images")}>
<Tr t={t.images} />
</button>
</div>
{#if mode === "map"} <Tr t={t.mapView} />
{#if $selectedElement !== undefined} </Tab>
<!-- right modal with the selected element view --> <Tab class={({ selected }) => ("tab "+ (selected ? "tab-selected" : "tab-unselected"))}>
<Drawer
placement="right" <Tr t={t.tableView} />
transitionType="fly" </Tab>
activateClickOutside={false} <Tab class={({ selected }) => ("tab "+ (selected ? "tab-selected" : "tab-unselected"))}>
backdrop={false}
id="drawer-right" <Tr t={t.aggregateView} />
width="w-full md:w-6/12 lg:w-5/12 xl:w-4/12" </Tab>
rightOffset="inset-y-0 right-0" <Tab class={({ selected }) => ("tab "+ (selected ? "tab-selected" : "tab-unselected"))}>
transitionParams={{
<Tr t={t.images} />
</Tab>
</TabList>
<TabPanels class="flex-grow flex flex-col">
<TabPanel class="flex-grow">
{#if $selectedElement !== undefined}
<!-- right modal with the selected element view -->
<Drawer
placement="right"
transitionType="fly"
activateClickOutside={false}
backdrop={false}
id="drawer-right"
width="w-full md:w-6/12 lg:w-5/12 xl:w-4/12"
rightOffset="inset-y-0 right-0"
transitionParams={{
x: 640, x: 640,
duration: 0, duration: 0,
easing: linear, easing: linear,
}} }}
divClass="overflow-y-auto z-50 bg-white" divClass="overflow-y-auto z-50 bg-white"
hidden={$selectedElement === undefined} hidden={$selectedElement === undefined}
on:close={() => { on:close={() => {
selectedElement.setData(undefined) selectedElement.setData(undefined)
}} }}
> >
<TitledPanel> <TitledPanel>
<div slot="title" class="flex justify-between"> <div slot="title" class="flex justify-between">
<a <a
target="_blank" target="_blank"
rel="noopener" rel="noopener"
href={"https://osm.org/" + $selectedElement.properties.id} href={"https://osm.org/" + $selectedElement.properties.id}
> >
{$selectedElement.properties.id} {$selectedElement.properties.id}
</a> </a>
<XCircleIcon class="h-6 w-6" on:click={() => selectedElement.set(undefined)} /> <XCircleIcon class="h-6 w-6" on:click={() => selectedElement.set(undefined)} />
</div> </div>
<History onlyShowChangesBy={$username.split(";")} id={$selectedElement.properties.id} /> <History onlyShowChangesBy={$username.split(";")} id={$selectedElement.properties.id} />
</TitledPanel> </TitledPanel>
</Drawer> </Drawer>
{/if}
<div class="relative m-1 flex-grow overflow-hidden rounded-xl">
<MaplibreMap {map} mapProperties={maplibremap} autorecovery={true} />
<div class="absolute right-0 top-0 w-1/4 p-4">
<Searchbar
on:search={() => search()}
isFocused={searchIsFocussed}
value={searchvalue}
on:focus={() => state.searchState.showSearchDrawer.set(true)}
/>
{#if $searchSuggestions?.length > 0 || $searchIsFocussed}
<GeocodeResults {state} on:select={(event) => search(event.detail)} />
{/if} {/if}
</div>
</div> <div class="relative m-1 h-full flex-grow overflow-hidden rounded-xl">
{:else if mode === "table"} <MaplibreMap {map} mapProperties={maplibremap} autorecovery={true} />
<div class="m-2 h-full overflow-y-auto"> <div class="absolute right-0 top-0 w-1/4 p-4">
{#each $featuresStore as f} <Searchbar
<History onlyShowChangesBy={$username?.split(";")} id={f.properties.id} /> on:search={() => search()}
{/each} isFocused={searchIsFocussed}
</div> value={searchvalue}
{:else if mode === "aggregate"} on:focus={() => state.searchState.showSearchDrawer.set(true)}
<div class="m-2 h-full overflow-y-auto"> />
<AggregateView features={$featuresStore} onlyShowUsername={$username?.split(";")} /> {#if $searchSuggestions?.length > 0 || $searchIsFocussed}
</div> <GeocodeResults {state} on:select={(event) => search(event.detail)} />
{:else if mode === "images"} {/if}
<div class="m-2 h-full overflow-y-auto"> </div>
<AggregateImages features={$featuresStore} onlyShowUsername={$username?.split(";")} /> </div>
</div>
{/if} </TabPanel>
<TabPanel class="overflow-auto grow-0">
{#each $featuresStore as f}
<History onlyShowChangesBy={$username?.split(";")} id={f.properties.id} />
{/each}
</TabPanel>
<TabPanel >
<AggregateView features={$featuresStore} onlyShowUsername={$username?.split(";")} />
</TabPanel>
<TabPanel>
<AggregateImages features={$featuresStore} onlyShowUsername={$username?.split(";")} />
</TabPanel>
</TabPanels>
</TabGroup>
</div> </div>
<Page shown={showPreviouslyVisited}> <Page shown={showPreviouslyVisited}>

View file

@ -30,6 +30,8 @@ import Tr from "../Base/Tr.svelte"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Marker from "../Map/Marker.svelte" import Marker from "../Map/Marker.svelte"
import { twJoin } from "tailwind-merge" import { twJoin } from "tailwind-merge"
import { Tag } from "../../Logic/Tags/Tag"
import { Lists } from "../../Utils/Lists"
class DirectionIndicatorVis extends SpecialVisualizationSvelte { class DirectionIndicatorVis extends SpecialVisualizationSvelte {
funcName = "direction_indicator" funcName = "direction_indicator"
@ -243,17 +245,24 @@ class PresetTypeSelect extends SpecialVisualizationSvelte {
console.warn("Trying to use the _original_ layer") console.warn("Trying to use the _original_ layer")
layer = state.theme.layers.find((l) => l.id === layer._basedOn) ?? layer layer = state.theme.layers.find((l) => l.id === layer._basedOn) ?? layer
} }
const allKeys = Lists.dedup(layer.presets.flatMap(preset => preset.tags.flatMap(tag => tag.usedKeys())))
const question: QuestionableTagRenderingConfigJson = { const question: QuestionableTagRenderingConfigJson = {
id: layer.id + "-type", id: layer.id + "-type",
question: t.question.translations, question: t.question.translations,
mappings: layer.presets.map((pr) => ({ mappings: layer.presets.map((pr) => {
if: new And(pr.tags).asJson(), const presetKeys = new Set(pr.tags.flatMap(t => t.key))
icon: "auto", const keysToRemove = allKeys.filter(k => !presetKeys.has(k)).map(k => new Tag(k, ""))
then: (pr.description ? t.typeDescription : t.typeTitle).Subs({ return ({
title: pr.title, if: new And([...pr.tags, ...keysToRemove]).asJson(),
description: pr.description, icon: "auto",
}).translations, then: (pr.description ? t.typeDescription : t.typeTitle).Subs({
})), title: pr.title,
description: pr.description,
}).translations,
})
}),
} }
if (question.mappings.length === 0) { if (question.mappings.length === 0) {
console.error("No mappings for preset_type_select, something went wrong") console.error("No mappings for preset_type_select, something went wrong")

View file

@ -12,6 +12,7 @@
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import EditButton from "./EditButton.svelte" import EditButton from "./EditButton.svelte"
import { Strings } from "../../../Utils/Strings"
export let config: TagRenderingConfig export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
@ -83,7 +84,7 @@
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting())) onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()))
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting())) onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()))
} }
let answerId = "answer-" + Utils.randomString(5) let answerId = "answer-" + Strings.randomString(5)
let debug = state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false) let debug = state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false)
let apiState: Store<string> = state?.osmConnection?.apiIsOnline ?? new ImmutableStore("online") let apiState: Store<string> = state?.osmConnection?.apiIsOnline ?? new ImmutableStore("online")

View file

@ -44,8 +44,9 @@
} }
function getAutoIcon(mapping: { readonly if?: TagsFilter }): Readonly<Record<string, string>> { function getAutoIcon(mapping: { readonly if?: TagsFilter }): Readonly<Record<string, string>> {
const ifTags = TagUtils.removeEmptyParts(mapping.if)
for (const preset of layer.presets) { for (const preset of layer.presets) {
if (!new And(preset.tags).shadows(mapping.if)) { if (!new And(preset.tags).shadows(ifTags)) {
continue continue
} }

View file

@ -1,12 +1,7 @@
import { ConfigMeta } from "./configMeta" import { ConfigMeta } from "./configMeta"
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { import { Conversion, ConversionMessage, DesugaringContext, Pipe } from "../../Models/ThemeConfig/Conversion/Conversion"
Conversion,
ConversionMessage,
DesugaringContext,
Pipe,
} from "../../Models/ThemeConfig/Conversion/Conversion"
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer" import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
import { PrevalidateTheme, ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation" import { PrevalidateTheme, ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation"
import { AllSharedLayers } from "../../Customizations/AllSharedLayers" import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
@ -26,6 +21,7 @@ import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderi
import { ValidateTheme } from "../../Models/ThemeConfig/Conversion/ValidateTheme" import { ValidateTheme } from "../../Models/ThemeConfig/Conversion/ValidateTheme"
import * as questions from "../../../public/assets/generated/layers/questions.json" import * as questions from "../../../public/assets/generated/layers/questions.json"
import { Lists } from "../../Utils/Lists" import { Lists } from "../../Utils/Lists"
import { Strings } from "../../Utils/Strings"
export interface HighlightedTagRendering { export interface HighlightedTagRendering {
path: ReadonlyArray<string | number> path: ReadonlyArray<string | number>
@ -431,7 +427,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
} }
if (!tr["id"] && !tr["override"]) { if (!tr["id"] && !tr["override"]) {
const qtr = <QuestionableTagRenderingConfigJson>tr const qtr = <QuestionableTagRenderingConfigJson>tr
let id = "" + i + "_" + Utils.randomString(5) let id = "" + i + "_" + Strings.randomString(5)
if (qtr?.freeform?.key) { if (qtr?.freeform?.key) {
id = qtr?.freeform?.key id = qtr?.freeform?.key
} else if (qtr.mappings?.[0]?.if) { } else if (qtr.mappings?.[0]?.if) {

View file

@ -31,9 +31,9 @@
<WikipediaArticle wikipediaDetails={_wikipediaStores[0]} /> <WikipediaArticle wikipediaDetails={_wikipediaStores[0]} />
{:else} {:else}
<TabGroup> <TabGroup>
<TabList> <TabList class="tablist">
{#each _wikipediaStores as store (store.tag)} {#each _wikipediaStores as store (store.tag)}
<Tab class={({ selected }) => (selected ? "tab-selected" : "tab-unselected")}> <Tab class={({ selected }) => ("tab "+ (selected ? "tab-selected" : "tab-unselected"))}>
<WikipediaTitle wikipediaDetails={store} /> <WikipediaTitle wikipediaDetails={store} />
</Tab> </Tab>
{/each} {/each}
@ -51,14 +51,5 @@
<style> <style>
/* Actually used, don't remove*/ /* Actually used, don't remove*/
.tab-selected {
background-color: rgb(59 130 246);
color: rgb(255 255 255);
}
/* Actually used, don't remove*/
.tab-unselected {
background-color: rgb(255 255 255);
color: rgb(0 0 0);
}
</style> </style>

View file

@ -1378,66 +1378,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
element.scrollIntoView({ behavior: "smooth", block: "nearest" }) element.scrollIntoView({ behavior: "smooth", block: "nearest" })
} }
/**
* Returns true if the contents of `a` are the same (and in the same order) as `b`.
* Might have false negatives in some cases
* @param a
* @param b
*/
public static sameList<T>(a: ReadonlyArray<T>, b: ReadonlyArray<T>) {
if (a == b) {
return true
}
if (a === undefined || a === null || b === undefined || b === null) {
return false
}
if (a.length !== b.length) {
return false
}
for (let i = 0; i < a.length; i++) {
const ai = a[i]
const bi = b[i]
if (ai == bi) {
continue
}
if (ai === bi) {
continue
}
return false
}
return true
}
public static SameObject<T>(a: T, b: T, ignoreKeys?: string[]): boolean {
if (a === b) {
return true
}
if (a === undefined || a === null || b === null || b === undefined) {
return false
}
if (typeof a === "object" && typeof b === "object") {
for (const aKey in a) {
if (!(aKey in b)) {
return false
}
}
for (const bKey in b) {
if (!(bKey in a)) {
return false
}
}
for (const k in a) {
if (!Utils.SameObject(a[k], b[k])) {
return false
}
}
return true
}
return false
}
/** /**
* *
* Utils.splitIntoSubstitutionParts("abc") // => [{message: "abc"}] * Utils.splitIntoSubstitutionParts("abc") // => [{message: "abc"}]
@ -1510,45 +1450,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
filename: path.substring(path.lastIndexOf("/") + 1), filename: path.substring(path.lastIndexOf("/") + 1),
} }
} }
/**
* Removes accents from a string
* @param str
* @constructor
*
* Utils.RemoveDiacritics("bâtiments") // => "batiments"
* Utils.RemoveDiacritics(undefined) // => undefined
*/
public static RemoveDiacritics(str?: string): string {
// See #1729
if (!str) {
return str
}
return str.normalize("NFD").replace(/\p{Diacritic}/gu, "")
}
/**
* Simplifies a string to increase the chance of a match
* @param str
* Utils.simplifyStringForSearch("abc def; ghi 564") // => "abcdefghi564"
* Utils.simplifyStringForSearch("âbc déf; ghi 564") // => "abcdefghi564"
* Utils.simplifyStringForSearch(undefined) // => undefined
*/
public static simplifyStringForSearch(str: string): string {
return Utils.RemoveDiacritics(str)
?.toLowerCase()
?.replace(/[^a-z0-9]/g, "")
}
public static randomString(length: number): string {
let result = ""
for (let i = 0; i < length; i++) {
const chr = Math.random().toString(36).substr(2, 3)
result += chr
}
return result
}
/** /**
* Recursively rewrites all keys from `+key`, `key+` and `=key` into `key * Recursively rewrites all keys from `+key`, `key+` and `=key` into `key
* *

View file

@ -43,12 +43,15 @@ export class Lists {
* Elements are returned in the same order as they appear in the lists. * Elements are returned in the same order as they appear in the lists.
* Null/Undefined is returned as is. If an empty array is given, a new empty array will be returned * Null/Undefined is returned as is. If an empty array is given, a new empty array will be returned
*/ */
public static dedup(arr: NonNullable<string[]>): NonNullable<string[]> public static dedup(arr: NonNullable<ReadonlyArray<string>>): NonNullable<string[]>
public static dedup(arr: undefined): undefined public static dedup(arr: undefined): undefined
public static dedup(arr: string[] | undefined): string[] | undefined public static dedup(arr: ReadonlyArray<string> | undefined): string[] | undefined
public static dedup(arr: string[]): string[] { public static dedup(arr: ReadonlyArray<string>): string[] {
if (arr === undefined || arr === null) { if (arr === undefined) {
return arr return undefined
}
if (arr === null) {
return null
} }
const newArr = [] const newArr = []
for (const string of arr) { for (const string of arr) {
@ -60,8 +63,8 @@ export class Lists {
} }
public static dedupT<T>(arr: ReadonlyArray<T>): T[] public static dedupT<T>(arr: ReadonlyArray<T>): T[]
public static dedupT<T>(arr: null): null public static dedupT(arr: null): null
public static dedupT<T>(arr: undefined): undefined public static dedupT(arr: undefined): undefined
public static dedupT<T>(arr: ReadonlyArray<T>): T[] { public static dedupT<T>(arr: ReadonlyArray<T>): T[] {
if (arr === undefined) { if (arr === undefined) {
return undefined return undefined
@ -160,7 +163,7 @@ export class Lists {
* Lists.duplicates(["a", "b","c","b","b"] // => ["b"] * Lists.duplicates(["a", "b","c","b","b"] // => ["b"]
* *
*/ */
public static duplicates(arr: string[]): string[] { public static duplicates(arr: ReadonlyArray<string>): string[] {
if (arr === undefined) { if (arr === undefined) {
return undefined return undefined
} }
@ -175,4 +178,34 @@ export class Lists {
return Array.from(duplicates) return Array.from(duplicates)
} }
/**
* Returns true if the contents of `a` are the same (and in the same order) as `b`.
* Might have false negatives in some cases
* @param a
* @param b
*/
public static sameList<T>(a: ReadonlyArray<T>, b: ReadonlyArray<T>): boolean {
if (a == b) {
return true
}
if (a === undefined || a === null || b === undefined || b === null) {
return false
}
if (a.length !== b.length) {
return false
}
for (let i = 0; i < a.length; i++) {
const ai = a[i]
const bi = b[i]
if (ai == bi) {
continue
}
if (ai === bi) {
continue
}
return false
}
return true
}
} }

33
src/Utils/Objects.ts Normal file
View file

@ -0,0 +1,33 @@
/**
* Various object-related utils
*/
export default class Objects {
public static sameObject<T>(a: T, b: T, ignoreKeys?: string[]): boolean {
if (a === b) {
return true
}
if (a === undefined || a === null || b === null || b === undefined) {
return false
}
if (typeof a === "object" && typeof b === "object") {
for (const aKey in a) {
if (!(aKey in b)) {
return false
}
}
for (const bKey in b) {
if (!(bKey in a)) {
return false
}
}
for (const k in a) {
if (!Objects.sameObject(a[k], b[k])) {
return false
}
}
return true
}
return false
}
}

View file

@ -21,4 +21,42 @@ export class Strings {
public static isEmojiFlag(string: string): boolean { public static isEmojiFlag(string: string): boolean {
return /[🇦-🇿]{2}/u.test(string) // flags, see https://stackoverflow.com/questions/53360006/detect-with-regex-if-emoji-is-country-flag return /[🇦-🇿]{2}/u.test(string) // flags, see https://stackoverflow.com/questions/53360006/detect-with-regex-if-emoji-is-country-flag
} }
/**
* Removes accents from a string
* @param str
* @constructor
*
* Strings.removeDiacritics("bâtiments") // => "batiments"
* Strings.removeDiacritics(undefined) // => undefined
*/
public static removeDiacritics(str?: string): string {
// See #1729
if (!str) {
return str
}
return str.normalize("NFD").replace(/\p{Diacritic}/gu, "")
}
/**
* Simplifies a string to increase the chance of a match
* @param str
* Strings.simplifyStringForSearch("abc def; ghi 564") // => "abcdefghi564"
* Strings.simplifyStringForSearch("âbc déf; ghi 564") // => "abcdefghi564"
* Strings.simplifyStringForSearch(undefined) // => undefined
*/
public static simplifyStringForSearch(str: string): string {
return Strings.removeDiacritics(str)
?.toLowerCase()
?.replace(/[^a-z0-9]/g, "")
}
public static randomString(length: number): string {
let result = ""
for (let i = 0; i < length; i++) {
const chr = Math.random().toString(36).substr(2, 3)
result += chr
}
return result
}
} }

View file

@ -293,6 +293,49 @@ button.unstyled, .button-unstyled button {
padding: 0; padding: 0;
} }
/****** Tablist elements *****/
.tablist {
margin: 0.25rem;
padding: 0.5rem;
border: 2px dashed var(--button-background-hover);
border-radius: 0.5rem;
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.tab {
border: unset;
border-radius: 0;
transition: all;
color: var(--foreground-color);
border-bottom: 2px solid var(--foreground-color);
font-weight: bold;
margin: 0.25rem;
padding: 0.25rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.tab-selected {
opacity: 100%;
background: var(--interactive-background);
}
/* Actually used, don't remove*/
.tab-unselected {
background: #00000000 !important;
opacity: 60%;
}
.tab-unselected:hover {
background: var(--interactive-background);
}
/******* Other input elements ******/ /******* Other input elements ******/
.hover-alert:hover { .hover-alert:hover {