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",
"questions",
"mastodon"

View file

@ -666,85 +666,7 @@
]
}
},
{
"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": "這個地方並沒有廁所"
}
}
]
},
"has_toilets",
{
"id": "caravansites-website",
"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

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": {
"render": "./assets/layers/parking_spaces/parking_space.svg",
"mappings": [
{
"if": {
"or": [
"access=private",
"access=no"
]
},
"then": "./assets/layers/parking_spaces/parking_space_private.svg"
},
{
"if": "parking_space=disabled",
"then": "./assets/layers/toilet/wheelchair.svg"
@ -99,7 +108,7 @@
],
"lineRendering": [
{
"color": "#696969",
"color": "dimgray",
"width": "1"
}
],
@ -295,6 +304,69 @@
"it": "Questo è un posto auto riservato al car sharing.",
"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."
},
"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",
"then": {
@ -329,4 +414,4 @@
"enableImproveAccuracy": true,
"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",
"addExtraTags": [
"brand=",
"brand:wikidata="
"brand:wikidata=",
"brand:wikipedia="
],
"then": {
"en": "Not part of a bigger brand",
@ -3657,4 +3658,4 @@
}
}
]
}
}

View file

@ -43,7 +43,8 @@
"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",
"it": "Questo negozio non ha un marchio specifico, non fa parte di una catena più grande",
"uk": "Цей магазин не має певного бренду, він не є частиною великої мережі"
}
},
"addExtraTags": [
"brand=",
"brand:wikidata=",
"brand:wikipedia="
]
}
]
},

View file

@ -87,6 +87,10 @@
"if": "camera:type=doorbell",
"then": "./assets/layers/surveillance_camera/doorbell.svg"
},
{
"if": "camera:type=panorama",
"then": "./assets/themes/surveillance/panorama.svg"
},
{
"if": "_direction:leftright=right",
"then": "./assets/themes/surveillance/cam_right.svg"
@ -110,6 +114,10 @@
]
},
"then": "50,35,center"
},
{
"if": "camera:type=panorama",
"then": "55,55,center"
}
],
"render": "35,35,center"
@ -407,6 +415,16 @@
"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",
"icon": {
@ -530,7 +548,7 @@
"da": "Hvilken form for overvågning er dette kamera?",
"de": "Was überwacht diese Kamera?",
"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?",
"nl": "Wat soort bewaking wordt hier uitgevoerd?",
"sl": "Kaj nadzoruje ta kamera?"

View file

@ -548,28 +548,7 @@
}
],
"filter": [
{
"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"
}
}
]
},
"shelter",
{
"id": "bench",
"options": [
@ -617,4 +596,4 @@
"tactile_paving"
],
"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進行的變動"
},
"icon": "./assets/svg/logo.svg",
"startZoom": 1,
"startLat": 0,
"startLon": 0,
"hideFromOverview": true,
"layers": [
{

View file

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

View file

@ -30,5 +30,13 @@
"Pieter Vander Vennet"
],
"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": {
"aboutCurrentThemeTitle": "About this map",
"aboutMapComplete": "About MapComplete",
"downloadApp": "Download the app for Android",
"filter": "Filter data",
"legal": "Legal notices",
"moreUtilsTitle": "Discover more",

View file

@ -2468,17 +2468,6 @@
},
"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": {
"question": "Aquest lloc té un lloc web?",
"render": "Lloc web oficial: <a href='{website}'>{website}</a>"
@ -5510,6 +5499,13 @@
}
}
},
"23": {
"options": {
"0": {
"question": "Amb refugi"
}
}
},
"3": {
"options": {
"0": {
@ -7996,6 +7992,9 @@
"mappings": {
"0": {
"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."
@ -11556,7 +11555,7 @@
"2": {
"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, ..."
}
},
@ -12439,13 +12438,6 @@
"transit_stops": {
"description": "Capa que mostra diferents tipus de parades de transport públic.",
"filter": {
"0": {
"options": {
"0": {
"question": "Amb refugi"
}
}
},
"1": {
"options": {
"0": {

View file

@ -2663,17 +2663,6 @@
},
"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": {
"question": "Má toto místo webové stránky?",
"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": {
"options": {
"0": {
@ -8623,6 +8619,9 @@
"mappings": {
"0": {
"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."
@ -12500,7 +12499,7 @@
"2": {
"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…"
}
},
@ -13546,13 +13545,6 @@
"transit_stops": {
"description": "Vrstva zobrazující různé typy zastávek veřejné dopravy.",
"filter": {
"0": {
"options": {
"0": {
"question": "S přístřeškem"
}
}
},
"1": {
"options": {
"0": {

View file

@ -1561,17 +1561,6 @@
},
"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": {
"question": "Har dette sted et websted?",
"render": "Officiel hjemmeside: <a href='{website}'>{website}</a>"
@ -3152,7 +3141,7 @@
"2": {
"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, …"
}
}

View file

@ -2439,17 +2439,6 @@
},
"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": {
"question": "Hat dieser Ort eine Webseite?",
"render": "Offizielle Webseite: <a href='{website}'>{website}</a>"
@ -5488,6 +5477,13 @@
}
}
},
"23": {
"options": {
"0": {
"question": "Mit Unterstand"
}
}
},
"3": {
"options": {
"0": {
@ -7960,6 +7956,9 @@
"mappings": {
"0": {
"then": "Dieser Parkplatz hat 1 Stellplatz."
},
"1": {
"then": "Dieser Parkplatz hat 1 Stellplatz."
}
},
"render": "Dieser Parkplatz hat {capacity} Stellplätze."
@ -11546,6 +11545,9 @@
"then": "Eine bewegliche Kamera"
},
"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, ..."
}
},
@ -12428,13 +12430,6 @@
"transit_stops": {
"description": "Ebene mit verschiedenen Arten von Haltestellen.",
"filter": {
"0": {
"options": {
"0": {
"question": "Mit Unterstand"
}
}
},
"1": {
"options": {
"0": {

View file

@ -531,6 +531,40 @@
"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": {
"description": "An open map of statues, busts, graffitis and other artwork all over the world",
"name": "Artworks",
@ -2663,17 +2697,6 @@
},
"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": {
"question": "Does this place have a website?",
"render": "Official website: <a href='{website}'>{website}</a>"
@ -5838,6 +5861,13 @@
}
}
},
"23": {
"options": {
"0": {
"question": "With a shelter"
}
}
},
"3": {
"options": {
"0": {
@ -7016,6 +7046,37 @@
"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": {
"description": "Map layer to show fire hydrants.",
"name": "Hydrants",
@ -8626,10 +8687,34 @@
"description": "Layer showing individual parking spaces.",
"name": "Parking Spaces",
"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": {
"mappings": {
"0": {
"then": "This parking space has 1 space."
},
"1": {
"then": "This parking space has 1 space."
}
},
"render": "This parking spaces has {capacity} spaces."
@ -8654,6 +8739,9 @@
"13": {
"then": "This is a parking space reserved for car sharing."
},
"14": {
"then": "This is a parking space reserved for women."
},
"2": {
"then": "This is a disabled parking space."
},
@ -8785,6 +8873,130 @@
"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": {
"description": "The layer showing picnic tables",
"name": "Picnic tables",
@ -12543,6 +12755,9 @@
"then": "A panning camera"
},
"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, …"
}
},
@ -13588,13 +13803,6 @@
"transit_stops": {
"description": "Layer showing different types of transit stops.",
"filter": {
"0": {
"options": {
"0": {
"question": "With a shelter"
}
}
},
"1": {
"options": {
"0": {

View file

@ -2270,17 +2270,6 @@
},
"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": {
"question": "¿Este lugar tiene una página web?",
"render": "Página web oficial: <a href='{website}'>{website}</a>"
@ -5162,6 +5151,13 @@
}
}
},
"23": {
"options": {
"0": {
"question": "Con refugio"
}
}
},
"3": {
"options": {
"0": {
@ -7594,6 +7590,9 @@
"mappings": {
"0": {
"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."
@ -10582,6 +10581,9 @@
},
"2": {
"then": "Una cámara panorámica"
},
"3": {
"then": "Una cámara de 360°"
}
},
"question": "¿Qué tipo de cámara es esta?"
@ -11317,13 +11319,6 @@
"transit_stops": {
"description": "Capa que muestra diferentes tipos de paradas de transporte.",
"filter": {
"0": {
"options": {
"0": {
"question": "Con refugio"
}
}
},
"1": {
"options": {
"0": {

View file

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

View file

@ -1873,17 +1873,6 @@
},
"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": {
"question": "Ce lieu a-til un site internet ?",
"render": "Site officiel : <a href='{website}'>{website}</a>"
@ -3609,6 +3598,13 @@
}
}
},
"23": {
"options": {
"0": {
"question": "Avec un abri"
}
}
},
"6": {
"options": {
"0": {
@ -6519,6 +6515,9 @@
},
"2": {
"then": "Une caméra panoramique"
},
"3": {
"then": "Une caméra 360°"
}
},
"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é…"
}
},
"question": "De quel genre de surveillance cette caméraest-elle ?"
"question": "De quel genre de surveillance cette caméra est-elle ?"
},
"Surveillance:zone": {
"mappings": {
@ -6916,13 +6915,6 @@
},
"transit_stops": {
"filter": {
"0": {
"options": {
"0": {
"question": "Avec un abri"
}
}
},
"1": {
"options": {
"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. ",
"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": {

View file

@ -196,16 +196,6 @@
},
"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": {
"question": "Tempat sini terada situs web?",
"render": "Situs resmi: <a href='{website}'>{website}</a>"

View file

@ -2632,17 +2632,6 @@
},
"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": {
"question": "Questo posto ha un sito web?",
"render": "Sito web ufficiale: <a href='{website}'>{website}</a>"
@ -5758,6 +5747,13 @@
}
}
},
"23": {
"options": {
"0": {
"question": "Con una pensilina"
}
}
},
"3": {
"options": {
"0": {
@ -8459,6 +8455,9 @@
"mappings": {
"0": {
"then": "Questo posto auto ha 1 spazio."
},
"1": {
"then": "Questo posto auto ha 1 spazio."
}
},
"render": "Questo posto auto ha {capacity} spazi."
@ -12109,7 +12108,7 @@
"2": {
"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, ..."
}
},
@ -13124,13 +13123,6 @@
"transit_stops": {
"description": "Livello che mostra diversi tipi di fermate dei mezzi pubblici.",
"filter": {
"0": {
"options": {
"0": {
"question": "Con una pensilina"
}
}
},
"1": {
"options": {
"0": {

View file

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

View file

@ -443,17 +443,6 @@
"question": "Hva heter dette stedet?",
"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": {
"question": "Har dette stedet en nettside?",
"render": "Offisiell nettside: <a href='{website}'>{website}</a>"

View file

@ -2546,17 +2546,6 @@
},
"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": {
"question": "Heeft deze plaats een website?",
"render": "Officiële website: : <a href='{website}'>{website}</a>"
@ -7622,10 +7611,34 @@
"description": "Laag met individuele parkeerplekken.",
"name": "Parkeerplekken",
"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": {
"mappings": {
"0": {
"then": "Deze parkeerplek heeft 1 plaats."
},
"1": {
"then": "Deze parkeerplek heeft 1 plaats."
}
},
"render": "Deze parkeerplek heeft {capacity} plaatsen."
@ -7650,6 +7663,9 @@
"13": {
"then": "Deze parkeerplek is gereserveerd voor autodelen."
},
"14": {
"then": "Deze parkeerplek is gereserveerd voor vrouwen."
},
"2": {
"then": "Dit is een gehandicaptenparkeerplaats."
},
@ -7780,6 +7796,130 @@
"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": {
"description": "Deze laag toont picknicktafels",
"name": "Picknicktafels",

View file

@ -1143,17 +1143,6 @@
},
"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": {
"question": "Czy to miejsce ma stronę internetową?",
"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?"
},
"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": {
"question": "Este lugar tem um website?",
"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?"
},
"caravansites-toilets": {
"mappings": {
"0": {
"then": "Este lugar tem banheiros"
},
"1": {
"then": "Este lugar não tem banheiros"
}
},
"question": "Este lugar tem banheiros?"
},
"caravansites-website": {
"question": "Este lugar tem um website?",
"render": "Site oficial: <a href='{website}'>{website}</a>"

View file

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

View file

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

View file

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

View file

@ -5444,6 +5444,47 @@ button.unstyled, .button-unstyled button {
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 ******/
.hover-alert:hover {

View file

@ -10,6 +10,7 @@ import { Changes } from "../Osm/Changes"
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import { WithChangesState } from "../../Models/ThemeViewState/WithChangesState"
import Objects from "../../Utils/Objects"
export default class SelectedElementTagsUpdater {
private static readonly metatags = new Set([
@ -160,7 +161,7 @@ export default class SelectedElementTagsUpdater {
const newGeometry = osmObject.asGeoJson()?.geometry
const oldFeature = state.indexedFeatures.featuresById.data.get(id)
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)
this.invalidateCache(s)
oldFeature.geometry = newGeometry

View file

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

View file

@ -6,7 +6,7 @@ import { Stores, UIEventSource } from "../../UIEventSource"
import { FeatureSource, IndexedFeatureSource } from "../FeatureSource"
import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription"
import { Feature } from "geojson"
import { Utils } from "../../../Utils"
import Objects from "../../../Utils/Objects"
export default class ChangeGeometryApplicator implements FeatureSource {
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
const change = changesForFeature[changesForFeature.length - 1]
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
newFeatures.push(feature)
continue

View file

@ -9,7 +9,7 @@ import { BBox } from "../../BBox"
import { OsmFeature } from "../../../Models/OsmFeature"
import { Lists } from "../../../Utils/Lists"
;("use strict")
("use strict")
/**
* 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
if (
this._lastQueryBBox !== undefined &&
Utils.sameList(this._layersToDownload.data, this._lastRequestedLayers) &&
Lists.sameList(this._layersToDownload.data, this._lastRequestedLayers) &&
requestedBounds.isContainedIn(this._lastQueryBBox)
) {
return undefined

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import Locale from "../../UI/i18n/Locale"
import { Utils } from "../../Utils"
import ThemeSearch from "./ThemeSearch"
import { Lists } from "../../Utils/Lists"
import { Strings } from "../../Utils/Strings"
export default class SearchUtils {
/** Applies special search terms, such as 'studio', 'osmcha', ...
@ -60,7 +61,7 @@ export default class SearchUtils {
const queryParts = query
.trim()
.split(" ")
.map((q) => Utils.simplifyStringForSearch(q))
.map((q) => Strings.simplifyStringForSearch(q))
let terms: string[]
if (Array.isArray(keywords)) {
terms = keywords
@ -74,7 +75,7 @@ export default class SearchUtils {
const q = queryParts[i]
let minDistance: number = 99
for (const term of termsAll) {
const d = Utils.levenshteinDistance(q, Utils.simplifyStringForSearch(term))
const d = Utils.levenshteinDistance(q, Strings.simplifyStringForSearch(term))
if (d < minDistance) {
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++) {
const ti = optimized[i]
const tj = optimized[j]
if (
!ti ||
!tj ||
typeof ti.shadows !== "function" ||
typeof tj.shadows !== "function"
) {
continue
}
if (ti.shadows(tj)) {
// if 'ti' is true, this implies 'tj' is always true as well.
// 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 false, then the entire expression will be false and it doesn't matter what 'tj' yields
optimized.splice(j, 1)
j--
} else if (tj.shadows(ti)) {
optimized.splice(i, 1)
i--

View file

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

View file

@ -994,11 +994,31 @@ export class TagUtils {
].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()
if (opt === true || opt === false) {
return opt
}
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 { Readable, Subscriber, Unsubscriber, Updater, Writable } from "svelte/store"
import { Lists } from "../Utils/Lists"
/**
* Various static utils
@ -66,7 +67,7 @@ export class Stores {
stable.setData(undefined)
return
}
if (Utils.sameList(stable.data, list)) {
if (Lists.sameList(stable.data, list)) {
return
}
stable.setData(list)

View file

@ -25,6 +25,7 @@ import { eliCategory } from "../../RasterLayerProperties"
import licenses from "../../../assets/generated/license_info.json"
import { Strings } from "../../../Utils/Strings"
import { Lists } from "../../../Utils/Lists"
import Objects from "../../../Utils/Objects"
export class ValidateLanguageCompleteness extends DesugaringStep<ThemeConfig> {
private readonly _languages: string[]
@ -1085,11 +1086,8 @@ export class DetectDuplicatePresets extends DesugaringStep<ThemeConfig> {
const presetBTags = optimizedTags[j]
const presetB = presets[j]
if (
Utils.SameObject(presetATags, presetBTags) &&
Utils.sameList(
presetA.preciseInput.snapToLayers,
presetB.preciseInput.snapToLayers
)
Objects.sameObject(presetATags, presetBTags) &&
Lists.sameList(presetA.preciseInput.snapToLayers, presetB.preciseInput.snapToLayers)
) {
context.err(
`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">
<TabList class="flex flex-wrap">
<div class="tablist sticky top-0 flex items-center justify-center">
<TabList class="flex items-center justify-center">
{#if $$slots.title0}
<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">
<slot name="title0">Tab 0</slot>
@ -56,7 +56,7 @@
{/if}
{#if $$slots.title1}
<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">
<slot name="title1" />
@ -65,7 +65,7 @@
{/if}
{#if $$slots.title2}
<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">
<slot name="title2" />
@ -74,7 +74,7 @@
{/if}
{#if $$slots.title3}
<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">
<slot name="title3" />
@ -83,7 +83,7 @@
{/if}
{#if $$slots.title4}
<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">
<slot name="title4" />
@ -92,7 +92,7 @@
{/if}
{#if $$slots.title5}
<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">
<slot name="title5" />
@ -101,7 +101,7 @@
{/if}
{#if $$slots.title6}
<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">
<slot name="title6" />
@ -167,19 +167,6 @@
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) {
align-items: center;
gap: 0.25rem;
@ -190,8 +177,4 @@
fill: var(--interactive-contrast);
}
:global(.tab-unselected) {
background-color: var(--background-color) !important;
color: var(--foreground-color) !important;
}
</style>

View file

@ -55,15 +55,13 @@
import ImageUploadQueue from "../../Logic/ImageProviders/ImageUploadQueue"
import QueuedImagesView from "../Image/QueuedImagesView.svelte"
import InsetSpacer from "../Base/InsetSpacer.svelte"
import UserCircle from "@rgossiaux/svelte-heroicons/solid/UserCircle"
import OfflineManagement from "./OfflineManagement.svelte"
import { GlobeEuropeAfrica } from "@babeard/svelte-heroicons/solid/GlobeEuropeAfrica"
import { onDestroy } from "svelte"
import Avatar from "../Base/Avatar.svelte"
import { SpecialVisualizationSvelte } from "../SpecialVisualization"
import ThemeViewState from "../../Models/ThemeViewState"
import { Changes } from "../../Logic/Osm/Changes"
import PendingChangesView from "./PendingChangesView.svelte"
import { DevicePhoneMobileIcon } from "@babeard/svelte-heroicons/solid"
export let state: {
favourites: FavouritesFeatureSource
@ -236,6 +234,12 @@
<Tr t={Translations.t.general.attribution.emailCreators} />
</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">
<Mastodon class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.followOnMastodon} />

View file

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

View file

@ -33,6 +33,9 @@
import GeocodeResults from "./Search/GeocodeResults.svelte"
import MagnifyingGlassCircle from "@babeard/svelte-heroicons/mini/MagnifyingGlassCircle"
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")
let username = QueryParameters.GetQueryParameter("user", undefined, "Inspect this user")
@ -49,7 +52,7 @@
new CoordinateSearch(),
new OpenLocationCodeSearch(),
new PhotonSearch(true, 2),
new PhotonSearch()
new PhotonSearch(),
)
let showSearchDrawer = new UIEventSource(true)
let searchIsFocussed = new UIEventSource(false)
@ -138,7 +141,7 @@
const overpass = new Overpass(
Constants.defaultOverpassUrls[0],
undefined,
user.split(";").map((user) => 'nw(user_touched:"' + user + '");')
user.split(";").map((user) => "nw(user_touched:\"" + user + "\");"),
)
if (!maplibremap.bounds.data) {
return
@ -161,8 +164,6 @@
return true
})
let mode: "map" | "table" | "aggregate" | "images" = "map"
let showPreviouslyVisited = new UIEventSource(true)
const t = Translations.t.inspector
@ -181,8 +182,8 @@
<div class="flex h-screen w-full flex-col">
<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">
<MagnifyingGlassCircle class="h-6 w-6" />
<Tr t={t.title} />
</h1>
<ValidatedInput
@ -207,89 +208,98 @@
</a>
</div>
<div class="flex">
<button class:primary={mode === "map"} on:click={() => (mode = "map")}>
<Tr t={t.mapView} />
</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>
<TabGroup class="flex-grow flex flex-col">
<TabList class="tablist">
<Tab class={({ selected }) => ("tab "+ (selected ? "tab-selected" : "tab-unselected"))}>
{#if mode === "map"}
{#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={{
<Tr t={t.mapView} />
</Tab>
<Tab class={({ selected }) => ("tab "+ (selected ? "tab-selected" : "tab-unselected"))}>
<Tr t={t.tableView} />
</Tab>
<Tab class={({ selected }) => ("tab "+ (selected ? "tab-selected" : "tab-unselected"))}>
<Tr t={t.aggregateView} />
</Tab>
<Tab class={({ selected }) => ("tab "+ (selected ? "tab-selected" : "tab-unselected"))}>
<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,
duration: 0,
easing: linear,
}}
divClass="overflow-y-auto z-50 bg-white"
hidden={$selectedElement === undefined}
on:close={() => {
divClass="overflow-y-auto z-50 bg-white"
hidden={$selectedElement === undefined}
on:close={() => {
selectedElement.setData(undefined)
}}
>
<TitledPanel>
<div slot="title" class="flex justify-between">
<a
target="_blank"
rel="noopener"
href={"https://osm.org/" + $selectedElement.properties.id}
>
{$selectedElement.properties.id}
</a>
<XCircleIcon class="h-6 w-6" on:click={() => selectedElement.set(undefined)} />
</div>
>
<TitledPanel>
<div slot="title" class="flex justify-between">
<a
target="_blank"
rel="noopener"
href={"https://osm.org/" + $selectedElement.properties.id}
>
{$selectedElement.properties.id}
</a>
<XCircleIcon class="h-6 w-6" on:click={() => selectedElement.set(undefined)} />
</div>
<History onlyShowChangesBy={$username.split(";")} id={$selectedElement.properties.id} />
</TitledPanel>
</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)} />
<History onlyShowChangesBy={$username.split(";")} id={$selectedElement.properties.id} />
</TitledPanel>
</Drawer>
{/if}
</div>
</div>
{:else if mode === "table"}
<div class="m-2 h-full overflow-y-auto">
{#each $featuresStore as f}
<History onlyShowChangesBy={$username?.split(";")} id={f.properties.id} />
{/each}
</div>
{:else if mode === "aggregate"}
<div class="m-2 h-full overflow-y-auto">
<AggregateView features={$featuresStore} onlyShowUsername={$username?.split(";")} />
</div>
{:else if mode === "images"}
<div class="m-2 h-full overflow-y-auto">
<AggregateImages features={$featuresStore} onlyShowUsername={$username?.split(";")} />
</div>
{/if}
<div class="relative m-1 h-full 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}
</div>
</div>
</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>
<Page shown={showPreviouslyVisited}>

View file

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

View file

@ -12,6 +12,7 @@
import { Utils } from "../../../Utils"
import { twMerge } from "tailwind-merge"
import EditButton from "./EditButton.svelte"
import { Strings } from "../../../Utils/Strings"
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
@ -83,7 +84,7 @@
onDestroy(highlightedRendering?.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 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>> {
const ifTags = TagUtils.removeEmptyParts(mapping.if)
for (const preset of layer.presets) {
if (!new And(preset.tags).shadows(mapping.if)) {
if (!new And(preset.tags).shadows(ifTags)) {
continue
}

View file

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

View file

@ -31,9 +31,9 @@
<WikipediaArticle wikipediaDetails={_wikipediaStores[0]} />
{:else}
<TabGroup>
<TabList>
<TabList class="tablist">
{#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} />
</Tab>
{/each}
@ -51,14 +51,5 @@
<style>
/* 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>

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" })
}
/**
* 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"}]
@ -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),
}
}
/**
* 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
*

View file

@ -43,12 +43,15 @@ export class 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
*/
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: string[] | undefined): string[] | undefined
public static dedup(arr: string[]): string[] {
if (arr === undefined || arr === null) {
return arr
public static dedup(arr: ReadonlyArray<string> | undefined): string[] | undefined
public static dedup(arr: ReadonlyArray<string>): string[] {
if (arr === undefined) {
return undefined
}
if (arr === null) {
return null
}
const newArr = []
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: null): null
public static dedupT<T>(arr: undefined): undefined
public static dedupT(arr: null): null
public static dedupT(arr: undefined): undefined
public static dedupT<T>(arr: ReadonlyArray<T>): T[] {
if (arr === undefined) {
return undefined
@ -160,7 +163,7 @@ export class Lists {
* Lists.duplicates(["a", "b","c","b","b"] // => ["b"]
*
*/
public static duplicates(arr: string[]): string[] {
public static duplicates(arr: ReadonlyArray<string>): string[] {
if (arr === undefined) {
return undefined
}
@ -175,4 +178,34 @@ export class Lists {
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 {
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;
}
/****** 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 ******/
.hover-alert:hover {