diff --git a/assets/layers/etymology/etymology.json b/assets/layers/etymology/etymology.json index 17bc04049..aee734eda 100644 --- a/assets/layers/etymology/etymology.json +++ b/assets/layers/etymology/etymology.json @@ -46,6 +46,7 @@ }, { "id": "wikipedia-etymology", + "condition": "name~*", "question": { "en": "What is the Wikidata-item that this object is named after?", "nl": "Wat is het Wikidata-item van hetgeen dit object is naar vernoemd?", diff --git a/assets/layers/filters/filters.json b/assets/layers/filters/filters.json index 9a390b950..551ef6897 100644 --- a/assets/layers/filters/filters.json +++ b/assets/layers/filters/filters.json @@ -244,6 +244,33 @@ } } ] + }, + { + "id": "dogs", + "options": [ + { + "question": { + "en": "No preference towards dogs" + } + }, + { + "question": { + "en": "Dogs allowed" + }, + "osmTags": { + "or": [ + "dog=unleashed", + "dog=yes" + ] + } + }, + { + "question": { + "en": "No dogs allowed" + }, + "osmTags": "dog=no" + } + ] } ] -} \ No newline at end of file +} diff --git a/assets/layers/food/food.json b/assets/layers/food/food.json index 5d2c36717..bac347058 100644 --- a/assets/layers/food/food.json +++ b/assets/layers/food/food.json @@ -149,7 +149,6 @@ }, "tagRenderings": [ "images", - "level", { "question": { "nl": "Wat is de naam van deze eetgelegenheid?", @@ -213,6 +212,7 @@ "email", "phone", "payment-options", + "level", "wheelchair-access", { "question": { @@ -1102,7 +1102,8 @@ }, "has_organic", "accepts_cash", - "accepts_cards" + "accepts_cards", + "dogs" ], "deletion": { "nonDeleteMappings": [ diff --git a/assets/layers/icons/icons.json b/assets/layers/icons/icons.json index 2a3304728..dd1efb4e3 100644 --- a/assets/layers/icons/icons.json +++ b/assets/layers/icons/icons.json @@ -154,6 +154,44 @@ } ], "condition": "id~(node|way|relation)/[0-9]*" + }, + { + "id": "dogicon", + "labels": [ + "defaults" + ], + "mappings": [ + { + "if": "dog=no", + "#": "ignore-image-in-then", + "then": "no_dogs" + }, + { + "if": "dog=leashed", + "#": "ignore-image-in-then", + "then": "dogs are allowed but leashed" + }, + { + "if": { + "or": [ + "dog=yes", + "dog=unleashed" + ] + }, + "#": "ignore-image-in-then", + "then": "dogs are allowed" + } + ] + }, + { + "id": "rating", + "labels": [ + "defaults" + ], + "icon": { + "class": "w-20 mx-1 flex items-center" + }, + "render": "{rating()}" } ], "mapRendering": null diff --git a/assets/layers/indoors/indoors.json b/assets/layers/indoors/indoors.json index d9aabbd81..f683eac31 100644 --- a/assets/layers/indoors/indoors.json +++ b/assets/layers/indoors/indoors.json @@ -89,9 +89,25 @@ } ] }, + "titleIcons": [ + "icons.defaults", + { + "render": "{ref}", + "condition": "ref~*" + }, + { + "mappings": [ + { + "if": "capacity~*", + "then": "
{capacity}
" + } + ] + } + ], "minzoom": 13, "tagRenderings": [ "images", + "level", { "id": "ref", "question": { @@ -162,7 +178,204 @@ "indoor=corridor" ] } - } + }, + { + "id": "room-type", + "question": { + "en": "What type of room is this?" + }, + "mappings": [ + { + "if": "room=administration", + "then": { + "en": "This is a administrative room" + }, + "icon": "./assets/layers/indoors/room_administration.svg" + }, + { + "if": "room=auditorium", + "then": { + "en": "This is a auditorium" + }, + "icon": "./assets/layers/indoors/room_auditorium.svg" + }, + { + "if": "room=bedroom", + "then": { + "en": "This is a bedroom" + }, + "icon": "./assets/layers/indoors/room_bedroom.svg" + }, + { + "if": "room=chapel", + "then": { + "en": "This is a chapel" + }, + "icon": "./assets/layers/indoors/room_chapel.svg" + }, + { + "if": "room=class", + "then": { + "en": "This is a classroom" + }, + "icon": "./assets/layers/indoors/room_class.svg" + }, + { + "if": "room=classroom", + "then": { + "en": "This is a classroom" + }, + "icon": "./assets/layers/indoors/room_class.svg", + "hideInAnswer": true + }, + { + "if": "room=computer", + "then": { + "en": "This is a computer room" + }, + "icon": "./assets/layers/indoors/room_computer.svg" + }, + { + "if": "room=conference", + "then": { + "en": "This is a conference room" + }, + "icon": "./assets/layers/indoors/room_conference.svg" + }, + { + "if": "room=crypt", + "then": { + "en": "This is a crypt" + }, + "icon": "./assets/layers/indoors/room_crypt.svg" + }, + { + "if": "room=kitchen", + "then": { + "en": "This is a kitchen" + }, + "icon": "./assets/layers/indoors/room_kitchen.svg" + }, + { + "if": "room=laboratory", + "then": { + "en": "This is a laboratory" + }, + "icon": "./assets/layers/indoors/room_laboratory.svg" + }, + { + "if": "room=library", + "then": { + "en": "This is a library" + }, + "icon": "./assets/layers/indoors/room_library.svg" + }, + { + "if": "room=locker", + "then": { + "en": "This is a locker room" + }, + "icon": "./assets/layers/indoors/room_locker.svg" + }, + { + "if": "room=nursery", + "then": { + "en": "This is a nursery" + }, + "icon": "./assets/layers/indoors/room_nursery.svg" + }, + { + "if": "room=office", + "then": { + "en": "This is an office" + }, + "icon": "./assets/layers/indoors/room_office.svg" + }, + { + "if": "room=prison_cell", + "then": { + "en": "This is a prison_cell" + }, + "icon": "./assets/layers/indoors/room_prison_cell.svg" + }, + { + "if": "room=restaurant", + "then": { + "en": "This is a restaurant" + }, + "icon": "./assets/layers/indoors/room_restaurant.svg" + }, + { + "if": "room=security_check", + "then": { + "en": "This is a room to perform security checks" + }, + "icon": "./assets/layers/indoors/room_security_check.svg" + }, + { + "if": "room=sport", + "then": { + "en": "This is a sport room" + }, + "icon": "./assets/layers/indoors/room_sport.svg" + }, + { + "if": "room=storage", + "then": { + "en": "This is a storage room" + }, + "icon": "./assets/layers/indoors/room_storage.svg" + }, + { + "if": "room=technical", + "then": { + "en": "This is a technical room" + }, + "icon": "./assets/layers/indoors/room_technical.svg" + }, + { + "if": "room=toilets", + "then": { + "en": "These are toilets" + }, + "icon": "./assets/layers/indoors/room_toilets.svg" + }, + { + "if": "room=waiting", + "then": { + "en": "This is a waiting room" + }, + "icon": "./assets/layers/indoors/room_waiting.svg" + } + ] + }, + { + "id": "room-capacity", + "question": { + "en": "How much people can at most fit in this room?" + }, + "condition": { + "or": [ + "room=waiting", + "room=restaurant", + "room=office", + "room=nursery", + "room=conference", + "room=auditorium", + "room=chapel", + "room=bedroom", + "room=classroom" + ] + }, + "render": { + "en": "At most {capacity} people fit this room" + }, + "freeform": { + "key": "capacity", + "type": "pnat" + } + }, + "etymology.wikipedia-etymology" ], "mapRendering": [ { @@ -222,7 +435,7 @@ { "if": { "or": [ - "room=adminstration", + "room=administration", "room=auditorium", "room=bedroom", "room=chapel", diff --git a/assets/layers/questions/dogs_allowed.svg b/assets/layers/questions/dogs_allowed.svg new file mode 100644 index 000000000..0f47ee8bf --- /dev/null +++ b/assets/layers/questions/dogs_allowed.svg @@ -0,0 +1,70 @@ + + + + + + + + + image/svg+xml + + + + + Openclipart + + + + + + + + + + + diff --git a/assets/layers/questions/dogs_allowed.svg.license b/assets/layers/questions/dogs_allowed.svg.license new file mode 100644 index 000000000..e14c126f7 --- /dev/null +++ b/assets/layers/questions/dogs_allowed.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: OpenClipArt +SPDX-License-Identifier: PD \ No newline at end of file diff --git a/assets/layers/questions/dogs_leashed.svg b/assets/layers/questions/dogs_leashed.svg new file mode 100644 index 000000000..d048b5d58 --- /dev/null +++ b/assets/layers/questions/dogs_leashed.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + diff --git a/assets/layers/questions/dogs_leashed.svg.license b/assets/layers/questions/dogs_leashed.svg.license new file mode 100644 index 000000000..e32d67ee6 --- /dev/null +++ b/assets/layers/questions/dogs_leashed.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: NPS Graphics, converted by User:ZyMOS +SPDX-License-Identifier: PD \ No newline at end of file diff --git a/assets/layers/questions/license_info.json b/assets/layers/questions/license_info.json index 65a360bb6..8985470b3 100644 --- a/assets/layers/questions/license_info.json +++ b/assets/layers/questions/license_info.json @@ -39,6 +39,26 @@ "https://www.onlinewebfonts.com/icon/464488" ] }, + { + "path": "dogs_allowed.svg", + "license": "PUBLIC-DOMAIN", + "authors": [ + "OpenClipArt" + ], + "sources": [ + "https://freesvg.org/no-dogs-round-sign-vector-graphics" + ] + }, + { + "path": "dogs_leashed.svg", + "license": "PUBLIC-DOMAIN", + "authors": [ + " \tNPS Graphics, converted by User:ZyMOS" + ], + "sources": [ + "https://commons.wikimedia.org/wiki/File:Pictograms-nps-pets_on_leash-2.svg" + ] + }, { "path": "nfc_card.svg", "license": "CC0-1.0", @@ -49,6 +69,16 @@ "https://wens.be/free-antwerpenize-bicycle-font" ] }, + { + "path": "no_dogs.svg", + "license": "Public Domain", + "authors": [ + "OpenClipArt" + ], + "sources": [ + "https://freesvg.org/no-dogs-round-sign-vector-graphics" + ] + }, { "path": "no_smoking.svg", "license": "CC0-1.0", diff --git a/assets/layers/questions/no_dogs.svg b/assets/layers/questions/no_dogs.svg new file mode 100644 index 000000000..888ae60de --- /dev/null +++ b/assets/layers/questions/no_dogs.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + + + + + + + + + diff --git a/assets/layers/questions/no_dogs.svg.license b/assets/layers/questions/no_dogs.svg.license new file mode 100644 index 000000000..8fc3e04c7 --- /dev/null +++ b/assets/layers/questions/no_dogs.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: OpenClipArt +SPDX-License-Identifier: Public Domain \ No newline at end of file diff --git a/assets/layers/questions/questions.json b/assets/layers/questions/questions.json index 2b351a49e..af7f5742d 100644 --- a/assets/layers/questions/questions.json +++ b/assets/layers/questions/questions.json @@ -130,7 +130,7 @@ "id": "reviews", "description": "Shows the reviews module (including the possibility to leave a review)", "render": { - "*": "{reviews()}" + "*": "{create_review()}{list_reviews()}" } }, { @@ -487,6 +487,7 @@ "mappings": [ { "if": "dog=yes", + "icon": "./assets/layers/questions/dogs_allowed.svg", "then": { "en": "Dogs are allowed", "nl": "honden zijn toegelaten", @@ -515,6 +516,7 @@ }, { "if": "dog=no", + "icon": "./assets/layers/questions/no_dogs.svg", "then": { "en": "Dogs are not allowed", "nl": "honden zijn niet toegelaten", @@ -542,6 +544,7 @@ }, { "if": "dog=leashed", + "icon": "./assets/layers/questions/dogs_leashed.svg", "then": { "en": "Dogs are allowed, but they have to be leashed", "nl": "honden zijn enkel aan de leiband welkom", @@ -568,6 +571,8 @@ }, { "if": "dog=unleashed", + "icon": "./assets/layers/questions/dogs_allowed.svg", + "then": { "en": "Dogs are allowed and can run around freely", "nl": "honden zijn welkom en mogen vrij rondlopen", @@ -1619,8 +1624,9 @@ }, { "id": "multilevels", - "builtin": "level", + "builtin": "single_level", "override": { + "=labels": [], "question": { "en": "What levels does this elevator go to?", "de": "Auf welchen Geschossen hält dieser Aufzug?", @@ -1657,7 +1663,22 @@ } }, { - "id": "level", + "id": "repeated", + "labels": [ + "level" + ], + "condition": "repeat_on~*", + "render": { + "en": "Multiple, identical objects can be found on floors {repeat_on}.", + "nl": "Er zijn verschillende, identieke objecten op verdiepingen {repeat_on}." + } + }, + { + "id": "single_level", + "labels": [ + "level" + ], + "condition": "repeat_on=", "question": { "nl": "Op welke verdieping bevindt dit punt zich?", "en": "On what level is this feature located?", diff --git a/assets/layers/shops/shops.json b/assets/layers/shops/shops.json index 8d6096d8e..ee0cf2bc1 100644 --- a/assets/layers/shops/shops.json +++ b/assets/layers/shops/shops.json @@ -348,6 +348,11 @@ "override": { "render": "./assets/layers/id_presets/maki-shop.svg", "+mappings": [ + { + "#": "Layer icon rendering", + "if": "id=", + "then": "circle:white;./assets/layers/id_presets/maki-shop.svg" + }, { "if": { "or": [ diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json index f84818f4c..d6643c1c3 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -723,6 +723,16 @@ "authors": [], "sources": [] }, + { + "path": "mangrove_logo.svg", + "license": "LOGO", + "authors": [ + "Mangrove.reviews" + ], + "sources": [ + "https://mangrove.reviews/" + ] + }, { "path": "mapcomplete_logo.svg", "license": "LOGO AND CC-BY-SA-4.0", diff --git a/assets/svg/mangrove_logo.svg b/assets/svg/mangrove_logo.svg new file mode 100644 index 000000000..f79997020 --- /dev/null +++ b/assets/svg/mangrove_logo.svg @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/assets/svg/star.svg b/assets/svg/star.svg index 61301ada9..84cc67ff2 100644 --- a/assets/svg/star.svg +++ b/assets/svg/star.svg @@ -1,6 +1,52 @@ - - - - - + + + + + diff --git a/assets/svg/star_half.svg b/assets/svg/star_half.svg index 7765c2872..c52a80d87 100644 --- a/assets/svg/star_half.svg +++ b/assets/svg/star_half.svg @@ -1,6 +1,56 @@ - - - - - - \ No newline at end of file + + + + + + + diff --git a/assets/svg/star_outline.svg b/assets/svg/star_outline.svg index 859b7539c..fa23c3705 100644 --- a/assets/svg/star_outline.svg +++ b/assets/svg/star_outline.svg @@ -1,6 +1,52 @@ - - - - - - \ No newline at end of file + + + + + + diff --git a/langs/ca.json b/langs/ca.json index 21c6d7381..7e9cc090b 100644 --- a/langs/ca.json +++ b/langs/ca.json @@ -534,10 +534,7 @@ "attribution": "Les ressenyes funcionen gràcies a Mangrove Reviews i estan disponibles sota CC-BY 4.0.", "i_am_affiliated": "Tinc alguna filiació amb aquest objecte
Marca-ho si n'ets cap, creador, treballador, …", "name_required": "És requerit un nom per mostrar i crear revisions", - "no_rating": "Doneu una puntuació abans d'enviar…", "no_reviews_yet": "No hi ha revisions encara. Sigues el primer a escriure'n una i ajuda al negoci i a les dades lliures!", - "plz_login": "Entra per deixar una revisió", - "posting_as": "Enviat com", "save": "Desar", "saved": "Revisió compartida. Gràcies per compartir!", "saving_review": "Desant…", diff --git a/langs/cs.json b/langs/cs.json index e5229e6b9..fe833def6 100644 --- a/langs/cs.json +++ b/langs/cs.json @@ -448,10 +448,7 @@ "attribution": "Recenze jsou poskytovány službou Mangrove Reviews a jsou k dispozici pod licencí CC-BY 4.0.", "i_am_affiliated": "Jsem spojen/a s tímto objektem
Zaškrtněte, pokud jste vlastníkem, tvůrcem, zaměstnancem, …", "name_required": "Pro zobrazení a vytváření recenzí je vyžadováno jméno", - "no_rating": "Před odesláním udělte hodnocení…", "no_reviews_yet": "Zatím zde nejsou žádné recenze. Buďte první, kdo ji napíše, a pomozte otevřít data a podnikání!", - "plz_login": "Přihlaste se a zanechte recenzi", - "posting_as": "Přihlášeni jako", "save": "Uložit", "saved": "Recenze uložena. Díky za sdílení!", "saving_review": "Ukládání…", diff --git a/langs/da.json b/langs/da.json index 1350677b5..76bd60c63 100644 --- a/langs/da.json +++ b/langs/da.json @@ -383,10 +383,7 @@ "attribution": "Anmeldelserne er baseret på Mangrove Reviews og er tilgængelige under CC-BY 4.0.", "i_am_affiliated": "Jeg er tilknyttet dette objekt
Tjek, om du er ejer, skaber, ansat, ...", "name_required": "Der kræves et navn for at vise og oprette anmeldelser", - "no_rating": "Ingen vurdering givet", "no_reviews_yet": "Der er ingen anmeldelser endnu. Vær den første til at skrive en og hjælpe åbne data og forretningen!", - "plz_login": "Log ind for at give en anmeldelse", - "posting_as": "Anmelder som", "saved": "Anmeldelse gemt. Tak for at bidrage!", "saving_review": "Gemmer…", "title": "{count} Anmeldelser", diff --git a/langs/de.json b/langs/de.json index 671f97a02..2c18b8185 100644 --- a/langs/de.json +++ b/langs/de.json @@ -536,10 +536,7 @@ "attribution": "Rezensionen werden bereitgestellt von Mangrove Reviews und sind unter CC-BY 4.0 verfügbar.", "i_am_affiliated": "Ich bin an diesem Objekt beteiligt
Auswählen, wenn Sie Eigentümer, Ersteller, Angestellter … sind", "name_required": "Der Name des Objekts ist erforderlich, um Bewertungen zu erstellen und anzuzeigen", - "no_rating": "Vor dem Absenden eine Bewertung abgeben…", "no_reviews_yet": "Es gibt noch keine Bewertungen. Hilf mit der ersten Bewertung dem Geschäft und der Open Data Bewegung!", - "plz_login": "Anmelden, um eine Bewertung abzugeben", - "posting_as": "Veröffentlichen als", "save": "Speichern", "saved": "Bewertung gespeichert. Danke fürs Teilen!", "saving_review": "Speichern…", diff --git a/langs/en.json b/langs/en.json index 42432d12c..7167afb95 100644 --- a/langs/en.json +++ b/langs/en.json @@ -197,6 +197,7 @@ "example": "Example", "examples": "Examples", "fewChangesBefore": "Please, answer a few questions of existing features before adding a new feature.", + "geopermissionDenied": "Using the geolocation was denied", "getStartedLogin": "Log in with OpenStreetMap to get started", "getStartedNewAccount": " or create a new account", "goToInbox": "Open inbox", @@ -556,14 +557,16 @@ "reviews": { "affiliated_reviewer_warning": "(Affiliated review)", "attribution": "Reviews are powered by Mangrove Reviews and are available under CC-BY 4.0.", - "i_am_affiliated": "I am affiliated with this object
Check if you are an owner, creator, employee, …", + "i_am_affiliated": "I am affiliated with this object", + "i_am_affiliated_explanation": "Check if you are an owner, creator, employee, …", "name_required": "A name is required in order to display and create reviews", - "no_rating": "Give a rating before submitting…", "no_reviews_yet": "There are no reviews yet. Be the first to write one and help open data and the business!", - "plz_login": "Log in to leave a review", - "posting_as": "Posting as", + "question": "How would you rate {title()}?", + "question_opinion": "How was your experience?", + "reviewing_as": "Reviewing as {nickname}", + "reviewing_as_anonymous": "Reviewing as anonymous", "save": "Save", - "saved": "Review saved. Thanks for sharing!", + "saved": "Review saved. Thanks for sharing!", "saving_review": "Saving…", "title": "{count} reviews", "title_singular": "One review", diff --git a/langs/es.json b/langs/es.json index 815880080..9522e2660 100644 --- a/langs/es.json +++ b/langs/es.json @@ -414,10 +414,7 @@ "reviews": { "affiliated_reviewer_warning": "(Revisión afiliada)", "name_required": "Se requiere un nombre para mostrar y crear comentarios", - "no_rating": "Da una calificación antes de enviar…", "no_reviews_yet": "Aún no hay reseñas. ¡Sé el primero en escribir una y ayuda a los datos abiertos y a los negocios!", - "plz_login": "Inicia sesión para dejar una reseña", - "posting_as": "Publicación como", "saved": "Reseña guardada. ¡Gracias por compartir!", "saving_review": "Guardando…", "title": "{count} comentarios", diff --git a/langs/fr.json b/langs/fr.json index 7d9422991..7bf0c5f9c 100644 --- a/langs/fr.json +++ b/langs/fr.json @@ -428,10 +428,7 @@ "attribution": "Les avis sont fournis par Mangrove Reviews et sont disponibles sous licence CC-BY 4.0.", "i_am_affiliated": "Je suis affilié à cet objet
Cochez si vous en êtes le propriétaire, créateur, employé, …", "name_required": "Un nom est requis pour afficher et créer des avis", - "no_rating": "Aucun score donné", "no_reviews_yet": "Il n'y a pas encore d'avis. Soyez le premier à en écrire un et aidez le lieu et les données ouvertes !", - "plz_login": "Connectez vous pour laisser un avis", - "posting_as": "Envoi en tant que", "saved": "Avis enregistré. Merci du partage !", "saving_review": "Enregistrement…", "title": "{count} avis", diff --git a/langs/gl.json b/langs/gl.json index 23648730b..8d9d04d00 100644 --- a/langs/gl.json +++ b/langs/gl.json @@ -163,10 +163,7 @@ "reviews": { "affiliated_reviewer_warning": "(Recensión de afiliado)", "name_required": "Requírese un nome para amosar e crear recensións", - "no_rating": "Sen puntuacións", "no_reviews_yet": "Non hai recensións aínda. Se o primeiro en escribir unha e axuda ao negocio e aos datos libres!", - "plz_login": "Inicia sesión para deixar unha recensión", - "posting_as": "Publicar como", "saved": "Recensión compartida. Grazas por compartir!", "saving_review": "Gardando…", "title": "{count} recensións", diff --git a/langs/hu.json b/langs/hu.json index de4faf548..625a81515 100644 --- a/langs/hu.json +++ b/langs/hu.json @@ -303,10 +303,7 @@ "attribution": "A véleményeket Mangrove Reviews tárolja, és a CC-BY 4.0 licenc szerint érhetők el.", "i_am_affiliated": "Kapcsolatban állok ezzel a létesítménnyel
Ellenőrizd, hogy tulajdonos, alkotó, alkalmazott vagy hasonló vagy-e.", "name_required": "Vélemények megjelenítéséhez és létrehozásához névre van szükség", - "no_rating": "Még nem kapott értékelést", "no_reviews_yet": "Még nincs vélemény. Légy Te az első, aki ír, és ezzel támogasd a nyílt adatokat és az üzletet!", - "plz_login": "Értékelés írásához jelentkezz be", - "posting_as": "Közzétéve mint", "saved": "Vélemény elmentve. Köszönjük a megosztást!", "saving_review": "Mentés…", "title": "{count} vélemény", diff --git a/langs/id.json b/langs/id.json index 69e86090a..419d2c101 100644 --- a/langs/id.json +++ b/langs/id.json @@ -162,9 +162,6 @@ }, "reviews": { "attribution": "Ulasan didukung oleh Mangrove Reviews dan tersedia di bawah CC-BY 4.0.", - "no_rating": "Tidak ada peringkat yang diberikan", - "plz_login": "Masuk untuk meninggalkan ulasan", - "posting_as": "Posting sebagai", "saved": " Ulasan disimpan. Terima kasih sudah berbagi! ", "saving_review": "Menyimpan…", "title": "{count} ulasan", diff --git a/langs/it.json b/langs/it.json index 1506a86b5..95464ae7a 100644 --- a/langs/it.json +++ b/langs/it.json @@ -307,10 +307,7 @@ "attribution": "Le recensioni sono fornite da Mangrove Reviews e sono disponibili con licenza CC-BY 4.0.", "i_am_affiliated": "Sono associato con questo oggetto
Spunta se sei il proprietario, creatore, dipendente, etc.", "name_required": "È richiesto un nome per poter mostrare e creare recensioni", - "no_rating": "Nessun voto ricevuto", "no_reviews_yet": "Non ci sono ancora recensioni. Sii il primo a scriverne una aiutando così i dati liberi e l’attività!", - "plz_login": "Accedi per lasciare una recensione", - "posting_as": "Pubblica come", "saved": "Recensione salvata. Grazie per averla condivisa!", "saving_review": "Salvataggio…", "title": "{count} recensioni", diff --git a/langs/ja.json b/langs/ja.json index afcfdd6ad..e09d9f1f9 100644 --- a/langs/ja.json +++ b/langs/ja.json @@ -165,10 +165,7 @@ "attribution": "レビューは、Mangrove Reviews and are available under CC-BY 4.0で公開されます。", "i_am_affiliated": "わたしは、この対象物の関係者です
所有者、作成者、従業員などの有無を確認します", "name_required": "レビューを表示および作成するには名前が必要です", - "no_rating": "評価が与えられていません", "no_reviews_yet": "まだレビューはありません。最初に書き込みを行い、データとビジネスのオープン化を支援しましょう!", - "plz_login": "ログインしてレビューを終了する", - "posting_as": "としての投稿", "saved": "レビューが保存されました。共有ありがとう!", "saving_review": "保存中…", "title": "{count}個のレビュー", diff --git a/langs/layers/ca.json b/langs/layers/ca.json index e3906df1d..f88322ac0 100644 --- a/langs/layers/ca.json +++ b/langs/layers/ca.json @@ -6410,7 +6410,7 @@ } } }, - "level": { + "single_level": { "mappings": { "0": { "then": "Situat a planta subterrani" @@ -6543,6 +6543,27 @@ }, "question": "Aquest servei té endolls elèctrics, disponibles pels clients quan hi són dins?" }, + "single_level": { + "mappings": { + "0": { + "then": "Situat a planta subterrani" + }, + "1": { + "then": "Situat a planta zero" + }, + "2": { + "then": "Situat a la planta zero" + }, + "3": { + "then": "Situat a primera planta" + }, + "4": { + "then": "Localitzat a la planta base" + } + }, + "question": "A quina planta està situat aquest element?", + "render": "Situat a la planta {level}" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/cs.json b/langs/layers/cs.json index 4c2da9751..7c241f168 100644 --- a/langs/layers/cs.json +++ b/langs/layers/cs.json @@ -1767,27 +1767,6 @@ "question": "Jaký je název sítě pro bezdrátový přístup k internetu?", "render": "Název sítě je {internet_access:ssid}" }, - "level": { - "mappings": { - "0": { - "then": "Nachází se v podzemí" - }, - "1": { - "then": "Nachází se v přízemí" - }, - "2": { - "then": "Nachází se v přízemí" - }, - "3": { - "then": "Nachází se v prvním patře" - }, - "4": { - "then": "Nachází se v prvním suterénu" - } - }, - "question": "V jaké úrovni se tento prvek nachází?", - "render": "Nachází se v {level}. patře" - }, "luminous_or_lit": { "mappings": { "0": { @@ -1897,6 +1876,27 @@ }, "question": "Má toto zařízení elektrické zásuvky, které jsou zákazníkům k dispozici, když jsou uvnitř?" }, + "single_level": { + "mappings": { + "0": { + "then": "Nachází se v podzemí" + }, + "1": { + "then": "Nachází se v přízemí" + }, + "2": { + "then": "Nachází se v přízemí" + }, + "3": { + "then": "Nachází se v prvním patře" + }, + "4": { + "then": "Nachází se v prvním suterénu" + } + }, + "question": "V jaké úrovni se tento prvek nachází?", + "render": "Nachází se v {level}. patře" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/da.json b/langs/layers/da.json index b47321dca..2378cec34 100644 --- a/langs/layers/da.json +++ b/langs/layers/da.json @@ -2163,27 +2163,6 @@ "question": "Hvad er netværksnavnet for den trådløse internetadgang?", "render": "Netværksnavnet er {internet_access:ssid}" }, - "level": { - "mappings": { - "0": { - "then": "Placeret under jorden" - }, - "1": { - "then": "Beliggende i stueetagen" - }, - "2": { - "then": "Beliggende i stueetagen" - }, - "3": { - "then": "Beliggende på første sal" - }, - "4": { - "then": "Beliggende på første kælderetage" - } - }, - "question": "På hvilket niveau er denne funktion placeret?", - "render": "Beliggende på {level}. etage" - }, "multilevels": { "override": { "question": "Hvilke niveauer går denne elevator til?", @@ -2237,6 +2216,27 @@ }, "question": "Har denne faciliteter stikkontakter tilgængelige for kunder, når de er inde?" }, + "single_level": { + "mappings": { + "0": { + "then": "Placeret under jorden" + }, + "1": { + "then": "Beliggende i stueetagen" + }, + "2": { + "then": "Beliggende i stueetagen" + }, + "3": { + "then": "Beliggende på første sal" + }, + "4": { + "then": "Beliggende på første kælderetage" + } + }, + "question": "På hvilket niveau er denne funktion placeret?", + "render": "Beliggende på {level}. etage" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/de.json b/langs/layers/de.json index e06449990..0af69a0bd 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -7345,27 +7345,6 @@ } } }, - "level": { - "mappings": { - "0": { - "then": "Das Objekt befindet sich unter der Erde" - }, - "1": { - "then": "Das Objekt befindet sich im Erdgeschoss" - }, - "2": { - "then": "Das Objekt befindet sich im Erdgeschoss" - }, - "3": { - "then": "Das Objekt befindet sich im 1. Obergeschoss" - }, - "4": { - "then": "Das Objekt befindet sich im 1. Untergeschoss" - } - }, - "question": "Auf welcher Ebene befindet sich das Objekt?", - "render": "Das Objekt befindet sich im {level}. Geschoss" - }, "luminous_or_lit": { "mappings": { "0": { @@ -7481,6 +7460,27 @@ }, "question": "Gibt es hier Steckdosen, an denen Kunden ihre Geräte laden können?" }, + "single_level": { + "mappings": { + "0": { + "then": "Das Objekt befindet sich unter der Erde" + }, + "1": { + "then": "Das Objekt befindet sich im Erdgeschoss" + }, + "2": { + "then": "Das Objekt befindet sich im Erdgeschoss" + }, + "3": { + "then": "Das Objekt befindet sich im 1. Obergeschoss" + }, + "4": { + "then": "Das Objekt befindet sich im 1. Untergeschoss" + } + }, + "question": "Auf welcher Ebene befindet sich das Objekt?", + "render": "Das Objekt befindet sich im {level}. Geschoss" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/en.json b/langs/layers/en.json index 54817046e..c0944b437 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -7405,27 +7405,6 @@ } } }, - "level": { - "mappings": { - "0": { - "then": "Located underground" - }, - "1": { - "then": "Located on the ground floor" - }, - "2": { - "then": "Located on the ground floor" - }, - "3": { - "then": "Located on the first floor" - }, - "4": { - "then": "Located on the first basement level" - } - }, - "question": "On what level is this feature located?", - "render": "Located on the {level}th floor" - }, "luminous_or_lit": { "mappings": { "0": { @@ -7524,6 +7503,9 @@ "phone": { "question": "What is the phone number of {title()}?" }, + "repeated": { + "render": "Multiple, identical objects can be found on floors {repeat_on}." + }, "service:electricity": { "mappings": { "0": { @@ -7541,6 +7523,27 @@ }, "question": "Does this amenity have electrical outlets, available to customers when they are inside?" }, + "single_level": { + "mappings": { + "0": { + "then": "Located underground" + }, + "1": { + "then": "Located on the ground floor" + }, + "2": { + "then": "Located on the ground floor" + }, + "3": { + "then": "Located on the first floor" + }, + "4": { + "then": "Located on the first basement level" + } + }, + "question": "On what level is this feature located?", + "render": "Located on the {level}th floor" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/eo.json b/langs/layers/eo.json index 978385bb9..2eb17199b 100644 --- a/langs/layers/eo.json +++ b/langs/layers/eo.json @@ -138,7 +138,13 @@ "email": { "question": "Kio estas la retpoŝta adreso de {title()}?" }, - "level": { + "opening_hours": { + "render": "

Malfermitaj horoj

{opening_hours_table(opening_hours)}" + }, + "phone": { + "question": "Kio estas la telefonnumero de {title()}?" + }, + "single_level": { "mappings": { "1": { "then": "En la teretaĝo" @@ -152,12 +158,6 @@ }, "render": "En la {level}a etaĝo" }, - "opening_hours": { - "render": "

Malfermitaj horoj

{opening_hours_table(opening_hours)}" - }, - "phone": { - "question": "Kio estas la telefonnumero de {title()}?" - }, "website": { "question": "Kie estas la retejo de {title()}?" } diff --git a/langs/layers/es.json b/langs/layers/es.json index 6fd2c0855..c95048d10 100644 --- a/langs/layers/es.json +++ b/langs/layers/es.json @@ -3631,27 +3631,6 @@ "question": "¿Cuál es el nombre de red para el acceso inalámbrico a internet?", "render": "El nombre de red es {internet_access:ssid}" }, - "level": { - "mappings": { - "0": { - "then": "Localizado bajo tierra" - }, - "1": { - "then": "Localizado en la planta baja" - }, - "2": { - "then": "Localizado en la planta baja" - }, - "3": { - "then": "Localizado en la primera planta" - }, - "4": { - "then": "Localizada en el primer sótano" - } - }, - "question": "¿En qué nivel se encuentra esta característica?", - "render": "Localizada en la {level}° planta" - }, "luminous_or_lit": { "mappings": { "0": { @@ -3764,6 +3743,27 @@ }, "question": "¿Esta facilidad tiene enchufes eléctricos, disponibles para los clientes cuando están dentro?" }, + "single_level": { + "mappings": { + "0": { + "then": "Localizado bajo tierra" + }, + "1": { + "then": "Localizado en la planta baja" + }, + "2": { + "then": "Localizado en la planta baja" + }, + "3": { + "then": "Localizado en la primera planta" + }, + "4": { + "then": "Localizada en el primer sótano" + } + }, + "question": "¿En qué nivel se encuentra esta característica?", + "render": "Localizada en la {level}° planta" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/fil.json b/langs/layers/fil.json index f77b2859e..f8763de68 100644 --- a/langs/layers/fil.json +++ b/langs/layers/fil.json @@ -34,27 +34,6 @@ "email": { "question": "Ano ang email address ng {title()}?" }, - "level": { - "mappings": { - "0": { - "then": "Nasa ilalim ng lupa" - }, - "1": { - "then": "Nasa unang palapag" - }, - "2": { - "then": "Nasa unang palapag" - }, - "3": { - "then": "Nasa unang palapag" - }, - "4": { - "then": "Nasa silong" - } - }, - "question": "Anong palapag matatagpuan ang tampók?", - "render": "Natagpuan sa ika-{level} na palapag" - }, "opening_hours": { "question": "Anong oras nagbubukas ang {title()}?", "render": "

Mga oras na bukas

{opening_hours_table(opening_hours)}" @@ -102,6 +81,27 @@ }, "question": "Merong bang mga intsupe (outlet) sa loob, para sa mga suki?" }, + "single_level": { + "mappings": { + "0": { + "then": "Nasa ilalim ng lupa" + }, + "1": { + "then": "Nasa unang palapag" + }, + "2": { + "then": "Nasa unang palapag" + }, + "3": { + "then": "Nasa unang palapag" + }, + "4": { + "then": "Nasa silong" + } + }, + "question": "Anong palapag matatagpuan ang tampók?", + "render": "Natagpuan sa ika-{level} na palapag" + }, "website": { "question": "Ano ang website ng {title()}?" }, diff --git a/langs/layers/fr.json b/langs/layers/fr.json index 15583d684..3f4a92964 100644 --- a/langs/layers/fr.json +++ b/langs/layers/fr.json @@ -4809,27 +4809,6 @@ } } }, - "level": { - "mappings": { - "0": { - "then": "En sous-sol" - }, - "1": { - "then": "Rez-de-chaussée" - }, - "2": { - "then": "Rez-de-chaussée" - }, - "3": { - "then": "Premier étage" - }, - "4": { - "then": "Sous-sol" - } - }, - "question": "À quel étage se situe l’élément ?", - "render": "Étage {level}" - }, "luminous_or_lit": { "mappings": { "0": { @@ -4939,6 +4918,27 @@ }, "question": "Des prises sont elles à disposition des client·e·s en intérieur ?" }, + "single_level": { + "mappings": { + "0": { + "then": "En sous-sol" + }, + "1": { + "then": "Rez-de-chaussée" + }, + "2": { + "then": "Rez-de-chaussée" + }, + "3": { + "then": "Premier étage" + }, + "4": { + "then": "Sous-sol" + } + }, + "question": "À quel étage se situe l’élément ?", + "render": "Étage {level}" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/hu.json b/langs/layers/hu.json index 44c9e796c..be2714d5d 100644 --- a/langs/layers/hu.json +++ b/langs/layers/hu.json @@ -798,27 +798,6 @@ "email": { "question": "Mi a(z) {title()} e-mail címe?" }, - "level": { - "mappings": { - "0": { - "then": "A föld alatt" - }, - "1": { - "then": "A földszinten" - }, - "2": { - "then": "A földszinten" - }, - "3": { - "then": "Az első emeleten" - }, - "4": { - "then": "Az első alagsori szinten" - } - }, - "question": "Melyik szinten található ez a létesítmény?", - "render": "A(z) {level}. emeleten" - }, "opening_hours": { "question": "Mikor van nyitva ez: {title()}?", "render": "

Nyitva tartás

{opening_hours_table(opening_hours)}" @@ -875,6 +854,27 @@ }, "question": "Van-e ebben a létesítményben olyan konnektor, amely a bent tartózkodó ügyfelek rendelkezésére áll?" }, + "single_level": { + "mappings": { + "0": { + "then": "A föld alatt" + }, + "1": { + "then": "A földszinten" + }, + "2": { + "then": "A földszinten" + }, + "3": { + "then": "Az első emeleten" + }, + "4": { + "then": "Az első alagsori szinten" + } + }, + "question": "Melyik szinten található ez a létesítmény?", + "render": "A(z) {level}. emeleten" + }, "website": { "question": "Mi a weboldala ennek: {title()}?" }, diff --git a/langs/layers/id.json b/langs/layers/id.json index ff82e7cdd..925438b30 100644 --- a/langs/layers/id.json +++ b/langs/layers/id.json @@ -503,27 +503,6 @@ "question": "Apa nama jaringan internet nirkabelnya?", "render": "Nama jaringan ini adalah {internet_access:ssid}" }, - "level": { - "mappings": { - "0": { - "then": "Terletak di bawah tanah" - }, - "1": { - "then": "Terletak di lantai dasar" - }, - "2": { - "then": "Terletak di lantai dasar" - }, - "3": { - "then": "Berlokasi di lantai pertama" - }, - "4": { - "then": "Terletak di lantai basement pertama" - } - }, - "question": "Pada tingkat apa fitur ini diletakkan?", - "render": "Terletak di lantai {level}" - }, "multilevels": { "override": { "question": "Pada lantai berapa saja lift ini berjalan?", @@ -569,6 +548,27 @@ "phone": { "question": "Berapa nomor telepon dari {title()}?" }, + "single_level": { + "mappings": { + "0": { + "then": "Terletak di bawah tanah" + }, + "1": { + "then": "Terletak di lantai dasar" + }, + "2": { + "then": "Terletak di lantai dasar" + }, + "3": { + "then": "Berlokasi di lantai pertama" + }, + "4": { + "then": "Terletak di lantai basement pertama" + } + }, + "question": "Pada tingkat apa fitur ini diletakkan?", + "render": "Terletak di lantai {level}" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/it.json b/langs/layers/it.json index 1b6502f5e..ea501f5a2 100644 --- a/langs/layers/it.json +++ b/langs/layers/it.json @@ -1809,24 +1809,6 @@ "email": { "question": "Qual è l'indirizzo email di {title()}?" }, - "level": { - "mappings": { - "0": { - "then": "Si trova sotto il livello stradale" - }, - "1": { - "then": "Si trova al pianoterra" - }, - "2": { - "then": "Si trova al pianoterra" - }, - "3": { - "then": "Si trova al primo piano" - } - }, - "question": "A quale piano si trova questo elemento?", - "render": "Si trova al piano numero {level}" - }, "opening_hours": { "question": "Quali sono gli orari di apertura di {title()}?", "render": "

Orari di apertura

{opening_hours_table(opening_hours)}" @@ -1854,6 +1836,24 @@ "phone": { "question": "Qual è il numero di telefono di {title()}?" }, + "single_level": { + "mappings": { + "0": { + "then": "Si trova sotto il livello stradale" + }, + "1": { + "then": "Si trova al pianoterra" + }, + "2": { + "then": "Si trova al pianoterra" + }, + "3": { + "then": "Si trova al primo piano" + } + }, + "question": "A quale piano si trova questo elemento?", + "render": "Si trova al piano numero {level}" + }, "website": { "question": "Qual è il sito web di {title()}?" }, diff --git a/langs/layers/ja.json b/langs/layers/ja.json index eb2c414f2..66ad27974 100644 --- a/langs/layers/ja.json +++ b/langs/layers/ja.json @@ -530,24 +530,6 @@ "email": { "question": "{title()}のEメールアドレスは何ですか?" }, - "level": { - "mappings": { - "0": { - "then": "地下にあります" - }, - "1": { - "then": "1階にあります" - }, - "2": { - "then": "1階にあります" - }, - "3": { - "then": "1階にあります" - } - }, - "question": "この機能は何階にあるのでしょうか?", - "render": "{level}階にあります" - }, "opening_hours": { "question": "{title()}の営業時間は?", "render": "

営業時間

{opening_hours_table(opening_hours)}" @@ -583,6 +565,24 @@ }, "question": "このアメニティにはコンセントがあり、お客様が店内にいるときにも利用できますか?" }, + "single_level": { + "mappings": { + "0": { + "then": "地下にあります" + }, + "1": { + "then": "1階にあります" + }, + "2": { + "then": "1階にあります" + }, + "3": { + "then": "1階にあります" + } + }, + "question": "この機能は何階にあるのでしょうか?", + "render": "{level}階にあります" + }, "website": { "question": "{title()}のウェブサイトは?" }, diff --git a/langs/layers/nb_NO.json b/langs/layers/nb_NO.json index d073d3e7e..4be55ca72 100644 --- a/langs/layers/nb_NO.json +++ b/langs/layers/nb_NO.json @@ -642,27 +642,6 @@ "question": "Hva er nettverksnavnet for det trådløse nettverket?", "render": "Nettverksnavnet er {internet_access:ssid}" }, - "level": { - "mappings": { - "0": { - "then": "Under bakken" - }, - "1": { - "then": "På gateplan" - }, - "2": { - "then": "På gateplan" - }, - "3": { - "then": "I andre etasje" - }, - "4": { - "then": "Er å finne på første kjellernivå" - } - }, - "question": "Hvilken etasje befinner funksjonen seg i?", - "render": "Ligger i {level} etasje" - }, "multilevels": { "override": { "question": "Hvilke etasjer går heisen til?", @@ -755,6 +734,27 @@ }, "question": "Har denne fasiliteten stikkontakter, tilgjengelig for kunder innendørs?" }, + "single_level": { + "mappings": { + "0": { + "then": "Under bakken" + }, + "1": { + "then": "På gateplan" + }, + "2": { + "then": "På gateplan" + }, + "3": { + "then": "I andre etasje" + }, + "4": { + "then": "Er å finne på første kjellernivå" + } + }, + "question": "Hvilken etasje befinner funksjonen seg i?", + "render": "Ligger i {level} etasje" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/nl.json b/langs/layers/nl.json index 391c14452..fb632fd5f 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -6881,27 +6881,6 @@ } } }, - "level": { - "mappings": { - "0": { - "then": "Bevindt zich ondergronds" - }, - "1": { - "then": "Bevindt zich op de begane grond" - }, - "2": { - "then": "Bevindt zich gelijkvloers" - }, - "3": { - "then": "Bevindt zich op de eerste verdieping" - }, - "4": { - "then": "Bevindt zich in de eerste kelderverdieping" - } - }, - "question": "Op welke verdieping bevindt dit punt zich?", - "render": "Bevindt zich op de {level}de verdieping" - }, "multilevels": { "override": { "question": "Naar welke verdiepingen gaat deze lift?", @@ -6980,6 +6959,9 @@ "phone": { "question": "Wat is het telefoonnummer van {title()}?" }, + "repeated": { + "render": "Er zijn verschillende, identieke objecten op verdiepingen {repeat_on}." + }, "service:electricity": { "mappings": { "0": { @@ -6997,6 +6979,27 @@ }, "question": "Zijn er stekkers beschikbaar voor klanten die binnen zitten?" }, + "single_level": { + "mappings": { + "0": { + "then": "Bevindt zich ondergronds" + }, + "1": { + "then": "Bevindt zich op de begane grond" + }, + "2": { + "then": "Bevindt zich gelijkvloers" + }, + "3": { + "then": "Bevindt zich op de eerste verdieping" + }, + "4": { + "then": "Bevindt zich in de eerste kelderverdieping" + } + }, + "question": "Op welke verdieping bevindt dit punt zich?", + "render": "Bevindt zich op de {level}de verdieping" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/pl.json b/langs/layers/pl.json index 606dc16b0..9ca723481 100644 --- a/langs/layers/pl.json +++ b/langs/layers/pl.json @@ -2405,27 +2405,6 @@ "question": "Jaka jest nazwa sieci dla bezprzewodowego dostępu do Internetu?", "render": "Nazwa sieci to {internet_access:ssid}" }, - "level": { - "mappings": { - "0": { - "then": "Znajduje się pod ziemią" - }, - "1": { - "then": "Znajduje się na parterze" - }, - "2": { - "then": "Znajduje się na parterze" - }, - "3": { - "then": "Znajduje się na pierwszym piętrze" - }, - "4": { - "then": "Położone na pierwszym poziomie piwnicy" - } - }, - "question": "Na jakim poziomie znajduje się ta funkcja?", - "render": "Znajduje się na {level} piętrze" - }, "luminous_or_lit": { "mappings": { "0": { @@ -2535,6 +2514,27 @@ }, "question": "Czy w tym przybytku znajdują się gniazdka elektryczne, gdzie klienci mogą naładować swoje urządzenia?" }, + "single_level": { + "mappings": { + "0": { + "then": "Znajduje się pod ziemią" + }, + "1": { + "then": "Znajduje się na parterze" + }, + "2": { + "then": "Znajduje się na parterze" + }, + "3": { + "then": "Znajduje się na pierwszym piętrze" + }, + "4": { + "then": "Położone na pierwszym poziomie piwnicy" + } + }, + "question": "Na jakim poziomie znajduje się ta funkcja?", + "render": "Znajduje się na {level} piętrze" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/pt.json b/langs/layers/pt.json index 0b555ee2d..835167ba8 100644 --- a/langs/layers/pt.json +++ b/langs/layers/pt.json @@ -882,27 +882,6 @@ "question": "Qual é o nome da rede para o acesso sem fios à Internet?", "render": "O nome da rede é {internet_access:ssid}" }, - "level": { - "mappings": { - "0": { - "then": "Está no subsolo" - }, - "1": { - "then": "Está ao nível do rés-do-chão" - }, - "2": { - "then": "Está ao nível do rés-do-chão" - }, - "3": { - "then": "Está no primeiro andar" - }, - "4": { - "then": "Localizado no primeiro nível da cave" - } - }, - "question": "Em que nível se encontra este elemento?", - "render": "Está no {level}º andar" - }, "multilevels": { "override": { "question": "Para que pisos vai este elevador?", @@ -956,6 +935,27 @@ }, "question": "Esta infraestrutura tem tomadas elétricas, disponíveis para os clientes quando estão no interior?" }, + "single_level": { + "mappings": { + "0": { + "then": "Está no subsolo" + }, + "1": { + "then": "Está ao nível do rés-do-chão" + }, + "2": { + "then": "Está ao nível do rés-do-chão" + }, + "3": { + "then": "Está no primeiro andar" + }, + "4": { + "then": "Localizado no primeiro nível da cave" + } + }, + "question": "Em que nível se encontra este elemento?", + "render": "Está no {level}º andar" + }, "smoking": { "mappings": { "0": { diff --git a/langs/layers/pt_BR.json b/langs/layers/pt_BR.json index a5a0a8ab2..c0e38375e 100644 --- a/langs/layers/pt_BR.json +++ b/langs/layers/pt_BR.json @@ -628,24 +628,6 @@ "email": { "question": "Qual o endereço de e-mail de {title()}?" }, - "level": { - "mappings": { - "0": { - "then": "Localizado no subsolo" - }, - "1": { - "then": "Localizado no térreo" - }, - "2": { - "then": "Localizado no térreo" - }, - "3": { - "then": "Localizado no primeiro andar" - } - }, - "question": "Em que nível esse recurso está localizado?", - "render": "Localizado no {level}o andar" - }, "opening_hours": { "question": "Qual o horário de funcionamento de {title()}?", "render": "

Horário de funcionamento

{opening_hours_table(opening_hours)}" @@ -664,6 +646,24 @@ "phone": { "question": "Qual o número de telefone de {title()}?" }, + "single_level": { + "mappings": { + "0": { + "then": "Localizado no subsolo" + }, + "1": { + "then": "Localizado no térreo" + }, + "2": { + "then": "Localizado no térreo" + }, + "3": { + "then": "Localizado no primeiro andar" + } + }, + "question": "Em que nível esse recurso está localizado?", + "render": "Localizado no {level}o andar" + }, "website": { "question": "Qual o site de {title()}?" }, diff --git a/langs/layers/ru.json b/langs/layers/ru.json index b32efc029..49cfa19c6 100644 --- a/langs/layers/ru.json +++ b/langs/layers/ru.json @@ -1558,24 +1558,6 @@ "email": { "question": "Какой адрес электронной почты у {title()}?" }, - "level": { - "mappings": { - "0": { - "then": "Расположено под землей" - }, - "1": { - "then": "Расположено на первом этаже" - }, - "2": { - "then": "Расположено на первом этаже" - }, - "3": { - "then": "Расположено на первом этаже" - } - }, - "question": "На каком этаже находится этот объект?", - "render": "Расположено на {level}ом этаже" - }, "opening_hours": { "question": "Какое время работы у {title()}?", "render": "

Часы работы

{opening_hours_table(opening_hours)}" @@ -1594,6 +1576,24 @@ "phone": { "question": "Какой номер телефона у {title()}?" }, + "single_level": { + "mappings": { + "0": { + "then": "Расположено под землей" + }, + "1": { + "then": "Расположено на первом этаже" + }, + "2": { + "then": "Расположено на первом этаже" + }, + "3": { + "then": "Расположено на первом этаже" + } + }, + "question": "На каком этаже находится этот объект?", + "render": "Расположено на {level}ом этаже" + }, "website": { "question": "Какой сайт у {title()}?" }, diff --git a/langs/layers/sl.json b/langs/layers/sl.json index 31e74dfef..279276a6b 100644 --- a/langs/layers/sl.json +++ b/langs/layers/sl.json @@ -149,26 +149,6 @@ "email": { "question": "Kakšen naslov elektronske pošte ima {title()}?" }, - "level": { - "mappings": { - "0": { - "then": "Nahaja se pod zemljo" - }, - "1": { - "then": "Nahaja se v pritličju" - }, - "2": { - "then": "Nahaja se v pritličju" - }, - "3": { - "then": "Nahaja se v prvem nadstropju" - }, - "4": { - "then": "Nahaja se v prvi kletni etaži" - } - }, - "render": "Nahaja se v {level}. nadstropju" - }, "opening_hours": { "question": "Kakšen odpiralni čas ima {title()}?", "render": "

Odpiralni čas

{opening_hours_table(opening_hours)}" @@ -198,6 +178,26 @@ }, "phone": { "question": "Kakšno telefonsko številko ima {title()}?" + }, + "single_level": { + "mappings": { + "0": { + "then": "Nahaja se pod zemljo" + }, + "1": { + "then": "Nahaja se v pritličju" + }, + "2": { + "then": "Nahaja se v pritličju" + }, + "3": { + "then": "Nahaja se v prvem nadstropju" + }, + "4": { + "then": "Nahaja se v prvi kletni etaži" + } + }, + "render": "Nahaja se v {level}. nadstropju" } } }, diff --git a/langs/layers/sv.json b/langs/layers/sv.json index 02d594aa4..fcace1943 100644 --- a/langs/layers/sv.json +++ b/langs/layers/sv.json @@ -51,24 +51,6 @@ "email": { "question": "Vad är e-postadressen till {title()}?" }, - "level": { - "mappings": { - "0": { - "then": "Ligger under jorden" - }, - "1": { - "then": "Ligger på bottenvåningen" - }, - "2": { - "then": "Ligger på bottenvåningen" - }, - "3": { - "then": "Ligger på första våningen" - } - }, - "question": "På vilken nivå finns den här funktionen?", - "render": "Ligger på {level}:e våningen" - }, "opening_hours": { "question": "Vilka är öppettiderna för {title()}?", "render": "

Öppettider

{opening_hours_table(opening_hours)}" @@ -104,6 +86,24 @@ }, "question": "Har den här bekvämligheten eluttag tillgängliga för kunder när de är inne?" }, + "single_level": { + "mappings": { + "0": { + "then": "Ligger under jorden" + }, + "1": { + "then": "Ligger på bottenvåningen" + }, + "2": { + "then": "Ligger på bottenvåningen" + }, + "3": { + "then": "Ligger på första våningen" + } + }, + "question": "På vilken nivå finns den här funktionen?", + "render": "Ligger på {level}:e våningen" + }, "website": { "question": "Vad är webbplatsen för {title()}?" }, diff --git a/langs/layers/zh_Hant.json b/langs/layers/zh_Hant.json index 1b07031cf..dd9c456cb 100644 --- a/langs/layers/zh_Hant.json +++ b/langs/layers/zh_Hant.json @@ -628,27 +628,6 @@ "email": { "question": "{title()} 的電子郵件地址是什麼?" }, - "level": { - "mappings": { - "0": { - "then": "位於地下" - }, - "1": { - "then": "位於 1 樓" - }, - "2": { - "then": "位於 1 樓" - }, - "3": { - "then": "位於 2 樓" - }, - "4": { - "then": "位於地下一樓" - } - }, - "question": "此圖徽位於哪個樓層/層級?", - "render": "位於 {level} 樓" - }, "opening_hours": { "question": "{title()} 的開放時間是什麼?", "render": "

開放時間

{opening_hours_table(opening_hours)}" @@ -705,6 +684,27 @@ }, "question": "這個便利設施有電器設備,能給客戶使用嗎?" }, + "single_level": { + "mappings": { + "0": { + "then": "位於地下" + }, + "1": { + "then": "位於 1 樓" + }, + "2": { + "then": "位於 1 樓" + }, + "3": { + "then": "位於 2 樓" + }, + "4": { + "then": "位於地下一樓" + } + }, + "question": "此圖徽位於哪個樓層/層級?", + "render": "位於 {level} 樓" + }, "website": { "question": "{title()} 網址是什麼?" }, diff --git a/langs/nb_NO.json b/langs/nb_NO.json index 3c56e2c00..af9d92472 100644 --- a/langs/nb_NO.json +++ b/langs/nb_NO.json @@ -401,10 +401,7 @@ "attribution": "Vurderinger er muliggjort av Mangrove Reviews og er tilgjengelige som CC-BY 4.0.", "i_am_affiliated": "Jeg har en tilknytning til dette objektet
Sjekk om du er eier, skaper, ansatt, …", "name_required": "Et navn kreves for å vise og opprette vurderinger", - "no_rating": "Ingen vurdering gitt", "no_reviews_yet": "Ingen vurderinger enda. Vær først til å skrive en og hjelp åpen data og bevegelsen.", - "plz_login": "Logg inn for å legge igjen en vurdering", - "posting_as": "Anmelder som", "saved": "Vurdering lagret. Takk for at du deler din mening.", "saving_review": "Lagrer …", "title": "{count} vurderinger", diff --git a/langs/nl.json b/langs/nl.json index 1a7e651cb..70101f0b2 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -530,10 +530,7 @@ "attribution": "De beoordelingen worden voorzien door Mangrove Reviews en zijn beschikbaar onder deCC-BY 4.0-licentie. ", "i_am_affiliated": "Ik ben persoonlijk betrokken
Vink aan indien je de oprichter, maker, werknemer, ... of dergelijke bent", "name_required": "De naam van dit object moet gekend zijn om een review te kunnen maken", - "no_rating": "Geef een beoordeling voordat je verzendt…", "no_reviews_yet": "Er zijn nog geen beoordelingen. Wees de eerste om een beoordeling te schrijven en help open data en het bedrijf!", - "plz_login": "Meld je aan om een beoordeling te geven", - "posting_as": "Ingelogd als", "save": "Opslaan", "saved": "Bedankt om je beoordeling te delen!", "saving_review": "Opslaan...", diff --git a/langs/pl.json b/langs/pl.json index d4d949a35..ca72241b4 100644 --- a/langs/pl.json +++ b/langs/pl.json @@ -331,10 +331,7 @@ "attribution": "Recenzje są obsługiwane przez Recenzje Mangrove i są dostępne na licencji CC-BY 4.0.", "i_am_affiliated": "Jestem powiązany z tym obiektem
Sprawdź czy jesteś właścicielem, twórcą, pracownikiem, ...", "name_required": "Nazwa jest wymagana do wyświetlania i tworzenia opinii", - "no_rating": "Nie podano oceny", "no_reviews_yet": "Nie ma jeszcze recenzji. Bądź pierwszym, który je napisze i pomóż otworzyć dane i biznes!", - "plz_login": "Zaloguj się, aby zostawić opinię", - "posting_as": "Publikowanie jako", "save": "Zapisz", "saved": "Opinia została zapisana. Dzięki za udostępnienie!", "saving_review": "Zapisywanie…", diff --git a/langs/pt.json b/langs/pt.json index 1b2767540..37f0f8c73 100644 --- a/langs/pt.json +++ b/langs/pt.json @@ -352,10 +352,7 @@ "attribution": "As avaliações são fornecidas por Mangrove Reviews e estão disponíveis sob a licença CC-BY 4.0.", "i_am_affiliated": "Eu sou afiliado a este objeto

Marque isto se for proprietário, criador, funcionário…
", "name_required": "É necessário um nome para mostrar e criar avaliações", - "no_rating": "Nenhuma classificação dada", "no_reviews_yet": "Ainda não existem avaliações. Seja o primeiro a escrever uma e ajude a abrir os dados e os negócios!", - "plz_login": "Inicie a sessão para deixar uma avaliação", - "posting_as": "Publicar como", "saved": "Avaliação guardada. Obrigado por partilhar!", "saving_review": "A guardar…", "title": "{count} avaliações", diff --git a/langs/pt_BR.json b/langs/pt_BR.json index d6af7cb4d..e84552603 100644 --- a/langs/pt_BR.json +++ b/langs/pt_BR.json @@ -162,10 +162,7 @@ "attribution": "As resenhas são fornecidas por Mangrove Reviews e estão disponíveis em CC-BY 4.0.", "i_am_affiliated": "Eu sou afiliado a este objeto

Verifique se você é proprietário, criador, funcionário, …
", "name_required": "É necessário um nome para exibir e criar comentários", - "no_rating": "Nenhuma classificação dada", "no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!", - "plz_login": "Entrar para deixar um comentário", - "posting_as": "Postando como", "saved": "Comentário salvo. Obrigado por compartilhar!", "saving_review": "Salvando…", "title": "{count} comentários", diff --git a/langs/ru.json b/langs/ru.json index aaa9683b7..ceb9b1378 100644 --- a/langs/ru.json +++ b/langs/ru.json @@ -176,10 +176,7 @@ "attribution": "Отзывы созданы на основе Mangrove Reviews и доступны под лицензией CC-BY 4.0.", "i_am_affiliated": "Я связан с этим объектом
Отметьте если вы создатель, владелец, работник, …", "name_required": "Необходимо название, чтобы просматривать и создавать отзывы", - "no_rating": "Нет рейтинга", "no_reviews_yet": "Пока нет отзывов. Оставьте первый отзыв и помогите открытым данным и бизнесу!", - "plz_login": "Войдите, чтобы оставить отзыв", - "posting_as": "Публикация от имени", "saved": " Отзыв сохранен. Спасибо, что поделились! ", "saving_review": "Сохранение…", "title": "{count} отзыв(-ов)", diff --git a/langs/zh_Hant.json b/langs/zh_Hant.json index 8772a0778..128b248a3 100644 --- a/langs/zh_Hant.json +++ b/langs/zh_Hant.json @@ -383,10 +383,7 @@ "attribution": "評審系統由Mangrove Reviews提供技術支援,採用CC-BY 4.0授權條款。", "i_am_affiliated": "我是這物件的相關關係者
確認你是否是擁有者、創造者、員工等等", "name_required": "需要有名稱才能顯示和創造審核", - "no_rating": "還沒有評分", "no_reviews_yet": "還沒有審核,當第一個撰寫者來幫助開放資料與商家吧!", - "plz_login": "登入來留下審核", - "posting_as": "以貼文", "saved": "已儲存審核,謝謝你的分享!", "saving_review": "儲存中…", "title": "{count} 審核次數", diff --git a/package.json b/package.json index 66bf5bccb..bc975ef3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.33.4", + "version": "0.33.5", "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", "bugs": "https://github.com/pietervdvn/MapComplete/issues", diff --git a/public/assets/mangrove_logo.png b/public/assets/mangrove_logo.png deleted file mode 100644 index 38f39f8ed..000000000 Binary files a/public/assets/mangrove_logo.png and /dev/null differ diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index da51a2a7f..7c9f0be3e 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -938,10 +938,6 @@ video { margin-bottom: 2rem; } -.ml-3 { - margin-left: 0.75rem; -} - .-ml-6 { margin-left: -1.5rem; } @@ -1210,11 +1206,6 @@ video { width: 2.5rem; } -.w-max { - width: -webkit-max-content; - width: max-content; -} - .w-48 { width: 12rem; } @@ -1404,6 +1395,12 @@ video { margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); } +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + .space-y-reverse > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 1; } @@ -1478,11 +1475,6 @@ video { text-overflow: clip; } -.break-normal { - overflow-wrap: normal; - word-break: normal; -} - .break-all { word-break: break-all; } @@ -1555,14 +1547,14 @@ video { border-width: 1px; } -.border-4 { - border-width: 4px; -} - .border-2 { border-width: 2px; } +.border-4 { + border-width: 4px; +} + .border-x { border-left-width: 1px; border-right-width: 1px; @@ -1669,10 +1661,6 @@ video { padding: 2rem; } -.p-1 { - padding: 0.25rem; -} - .p-2 { padding: 0.5rem; } @@ -1681,6 +1669,10 @@ video { padding: 1rem; } +.p-1 { + padding: 0.25rem; +} + .p-0\.5 { padding: 0.125rem; } @@ -1773,10 +1765,6 @@ video { text-align: justify; } -.align-middle { - vertical-align: middle; -} - .text-xl { font-size: 1.25rem; line-height: 1.75rem; @@ -1787,16 +1775,6 @@ video { line-height: 1.75rem; } -.text-4xl { - font-size: 2.25rem; - line-height: 2.5rem; -} - -.text-sm { - font-size: 0.875rem; - line-height: 1.25rem; -} - .text-3xl { font-size: 1.875rem; line-height: 2.25rem; @@ -1807,11 +1785,21 @@ video { line-height: 2rem; } +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + .text-base { font-size: 1rem; line-height: 1.5rem; } +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} + .font-bold { font-weight: 700; } @@ -1891,10 +1879,6 @@ video { font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction); } -.leading-none { - line-height: 1; -} - .tracking-tight { letter-spacing: -0.025em; } @@ -2662,26 +2646,6 @@ a.link-underline { opacity: 1; } -@media (prefers-reduced-motion: no-preference) { - @-webkit-keyframes spin { - to { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } - } - @keyframes spin { - to { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } - } - - .motion-safe\:animate-spin { - -webkit-animation: spin 1s linear infinite; - animation: spin 1s linear infinite; - } -} - @media (max-width: 480px) { .max-\[480px\]\:w-full { width: 100%; @@ -2816,10 +2780,6 @@ a.link-underline { height: 4rem; } - .md\:h-12 { - height: 3rem; - } - .md\:w-8 { width: 2rem; } diff --git a/scripts/build.sh b/scripts/build.sh index 737f929d4..3b1cf07b8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -12,8 +12,8 @@ mkdir dist/assets 2> /dev/null export NODE_OPTIONS="--max-old-space-size=8192" # This script ends every line with '&&' to chain everything. A failure will thus stop the build -# npm run generate:editor-layer-index && -# npm run generate && +npm run generate:editor-layer-index && +npm run generate && npm run generate:layouts if [ $? -ne 0 ]; then diff --git a/scripts/hetzner/deployHetzner.sh b/scripts/hetzner/deployHetzner.sh index 8d692a40c..6f1044d22 100755 --- a/scripts/hetzner/deployHetzner.sh +++ b/scripts/hetzner/deployHetzner.sh @@ -21,3 +21,4 @@ scp -r dist.zip hetzner:/root/ && scp ./scripts/hetzner/config/* hetzner:/root/ ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" rm dist.zip +npm run clean diff --git a/src/Logic/SimpleMetaTagger.ts b/src/Logic/SimpleMetaTagger.ts index f9f4997ff..e6c671328 100644 --- a/src/Logic/SimpleMetaTagger.ts +++ b/src/Logic/SimpleMetaTagger.ts @@ -339,21 +339,37 @@ export default class SimpleMetaTaggers { ) private static levels = new InlineMetaTagger( { - doc: "Extract the 'level'-tag into a normalized, ';'-separated value", + doc: "Extract the 'level'-tag into a normalized, ';'-separated value called '_level' (which also includes 'repeat_on'). The `level` tag (without underscore) will be normalized with only the value of `level`.", keys: ["_level"], }, (feature) => { - if (feature.properties["level"] === undefined) { - return false + let somethingChanged = false + if (feature.properties["level"] !== undefined) { + const l = feature.properties["level"] + const newValue = TagUtils.LevelsParser(l).join(";") + if (l !== newValue) { + feature.properties["level"] = newValue + somethingChanged = true + } } - const l = feature.properties["level"] - const newValue = TagUtils.LevelsParser(l).join(";") - if (l === newValue) { - return false + if (feature.properties["repeat_on"] !== undefined) { + const l = feature.properties["repeat_on"] + const newValue = TagUtils.LevelsParser(l).join(";") + if (l !== newValue) { + feature.properties["repeat_on"] = newValue + somethingChanged = true + } } - feature.properties["level"] = newValue - return true + + const combined = TagUtils.LevelsParser( + (feature.properties.repeat_on ?? "") + ";" + (feature.properties.level ?? "") + ).join(";") + if (feature.properties["_level"] !== combined) { + feature.properties["_level"] = combined + somethingChanged = true + } + return somethingChanged } ) private static canonicalize = new InlineMetaTagger( diff --git a/src/Logic/State/LayerState.ts b/src/Logic/State/LayerState.ts index 9354cb407..f8fb30761 100644 --- a/src/Logic/State/LayerState.ts +++ b/src/Logic/State/LayerState.ts @@ -66,11 +66,11 @@ export default class LayerState { } const t = Translations.t.general.levelSelection const conditionsOrred = [ - new Tag("level", "" + level), - new RegexTag("level", new RegExp("(.*;)?" + level + "(;.*)?")), + new Tag("_level", "" + level), + new RegexTag("_level", new RegExp("(.*;)?" + level + "(;.*)?")), ] if (level === "0") { - conditionsOrred.push(new Tag("level", "")) // No level tag is the same as level '0' + conditionsOrred.push(new Tag("_level", "")) // No level tag is the same as level '0' } console.log("Setting levels filter to", conditionsOrred) this.globalFilters.data.push({ diff --git a/src/Logic/Tags/TagUtils.ts b/src/Logic/Tags/TagUtils.ts index f319f1316..1c37457fe 100644 --- a/src/Logic/Tags/TagUtils.ts +++ b/src/Logic/Tags/TagUtils.ts @@ -483,13 +483,22 @@ export class TagUtils { * TagUtils.LevelsParser("-1") // => ["-1"] * TagUtils.LevelsParser("0;-1") // => ["0", "-1"] * TagUtils.LevelsParser(undefined) // => [] + * TagUtils.LevelsParser("") // => [] + * TagUtils.LevelsParser(";") // => [] + * */ public static LevelsParser(level: string): string[] { + if (level === undefined || level === null) { + return [] + } let spec = Utils.NoNull([level]) spec = [].concat(...spec.map((s) => s?.split(";"))) spec = [].concat( ...spec.map((s) => { s = s.trim() + if (s === "") { + return undefined + } if (s.indexOf("-") < 0 || s.startsWith("-")) { return s } diff --git a/src/Logic/Web/MangroveReviews.ts b/src/Logic/Web/MangroveReviews.ts index ec16f3445..15819a735 100644 --- a/src/Logic/Web/MangroveReviews.ts +++ b/src/Logic/Web/MangroveReviews.ts @@ -1,34 +1,35 @@ -import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" -import { MangroveReviews, Review } from "mangrove-reviews-typescript" -import { Utils } from "../../Utils" -import { Feature, Position } from "geojson" -import { GeoOperations } from "../GeoOperations" +import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"; +import { MangroveReviews, Review } from "mangrove-reviews-typescript"; +import { Utils } from "../../Utils"; +import { Feature, Position } from "geojson"; +import { GeoOperations } from "../GeoOperations"; export class MangroveIdentity { - public readonly keypair: Store - public readonly key_id: Store + public readonly keypair: Store; + public readonly key_id: Store; constructor(mangroveIdentity: UIEventSource) { - const key_id = new UIEventSource(undefined) - this.key_id = key_id - const keypairEventSource = new UIEventSource(undefined) - this.keypair = keypairEventSource + const key_id = new UIEventSource(undefined); + this.key_id = key_id; + const keypairEventSource = new UIEventSource(undefined); + this.keypair = keypairEventSource; mangroveIdentity.addCallbackAndRunD(async (data) => { if (data === "") { - return + return; } - const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data)) - keypairEventSource.setData(keypair) - const pem = await MangroveReviews.publicToPem(keypair.publicKey) - key_id.setData(pem) - }) + const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data)); + keypairEventSource.setData(keypair); + const pem = await MangroveReviews.publicToPem(keypair.publicKey); + key_id.setData(pem); + }); try { if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") { - MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {}) + MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => { + }); } } catch (e) { - console.error("Could not create identity: ", e) + console.error("Could not create identity: ", e); } } @@ -38,13 +39,13 @@ export class MangroveIdentity { * @constructor */ private static async CreateIdentity(identity: UIEventSource): Promise { - const keypair = await MangroveReviews.generateKeypair() - const jwk = await MangroveReviews.keypairToJwk(keypair) + const keypair = await MangroveReviews.generateKeypair(); + const jwk = await MangroveReviews.keypairToJwk(keypair); if ((identity.data ?? "") !== "") { // Identity has been loaded via osmPreferences by now - we don't overwrite - return + return; } - identity.setData(JSON.stringify(jwk)) + identity.setData(JSON.stringify(jwk)); } } @@ -52,17 +53,18 @@ export class MangroveIdentity { * Tracks all reviews of a given feature, allows to create a new review */ export default class FeatureReviews { - private static readonly _featureReviewsCache: Record = {} - public readonly subjectUri: Store + private static readonly _featureReviewsCache: Record = {}; + public readonly subjectUri: Store; + public readonly average: Store; private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store })[]> = - new UIEventSource([]) + new UIEventSource([]); public readonly reviews: Store<(Review & { madeByLoggedInUser: Store })[]> = - this._reviews - private readonly _lat: number - private readonly _lon: number - private readonly _uncertainty: number - private readonly _name: Store - private readonly _identity: MangroveIdentity + this._reviews; + private readonly _lat: number; + private readonly _lon: number; + private readonly _uncertainty: number; + private readonly _name: Store; + private readonly _identity: MangroveIdentity; private constructor( feature: Feature, @@ -75,55 +77,72 @@ export default class FeatureReviews { } ) { const centerLonLat = GeoOperations.centerpointCoordinates(feature) - ;[this._lon, this._lat] = centerLonLat + ;[this._lon, this._lat] = centerLonLat; this._identity = - mangroveIdentity ?? new MangroveIdentity(new UIEventSource(undefined)) - const nameKey = options?.nameKey ?? "name" + mangroveIdentity ?? new MangroveIdentity(new UIEventSource(undefined)); + const nameKey = options?.nameKey ?? "name"; if (feature.geometry.type === "Point") { - this._uncertainty = options?.uncertaintyRadius ?? 10 + this._uncertainty = options?.uncertaintyRadius ?? 10; } else { - let coordss: Position[][] + let coordss: Position[][]; if (feature.geometry.type === "LineString") { - coordss = [feature.geometry.coordinates] + coordss = [feature.geometry.coordinates]; } else if ( feature.geometry.type === "MultiLineString" || feature.geometry.type === "Polygon" ) { - coordss = feature.geometry.coordinates + coordss = feature.geometry.coordinates; } - let maxDistance = 0 + let maxDistance = 0; for (const coords of coordss) { for (const coord of coords) { maxDistance = Math.max( maxDistance, GeoOperations.distanceBetween(centerLonLat, coord) - ) + ); } } - this._uncertainty = options?.uncertaintyRadius ?? maxDistance + this._uncertainty = options?.uncertaintyRadius ?? maxDistance; } - this._name = tagsSource.map((tags) => tags[nameKey] ?? options?.fallbackName) + this._name = tagsSource.map((tags) => tags[nameKey] ?? options?.fallbackName); - this.subjectUri = this.ConstructSubjectUri() + this.subjectUri = this.ConstructSubjectUri(); - const self = this + const self = this; this.subjectUri.addCallbackAndRunD(async (sub) => { - const reviews = await MangroveReviews.getReviews({ sub }) - self.addReviews(reviews.reviews) - }) + const reviews = await MangroveReviews.getReviews({ sub }); + self.addReviews(reviews.reviews); + }); /* We also construct all subject queries _without_ encoding the name to work around a previous bug * See https://github.com/giggls/opencampsitemap/issues/30 */ this.ConstructSubjectUri(true).addCallbackAndRunD(async (sub) => { try { - const reviews = await MangroveReviews.getReviews({ sub }) - self.addReviews(reviews.reviews) + const reviews = await MangroveReviews.getReviews({ sub }); + self.addReviews(reviews.reviews); } catch (e) { - console.log("Could not fetch reviews for partially incorrect query ", sub) + console.log("Could not fetch reviews for partially incorrect query ", sub); } - }) + }); + this.average = this._reviews.map(reviews => { + if (!reviews) { + return null; + } + if(reviews.length === 0){ + return null + } + let sum = 0; + let count = 0; + for (const review of reviews) { + if (review.rating !== undefined) { + count++; + sum += review.rating; + } + } + return Math.round(sum / count) + }); } /** @@ -139,14 +158,14 @@ export default class FeatureReviews { uncertaintyRadius?: number } ) { - const key = feature.properties.id - const cached = FeatureReviews._featureReviewsCache[key] + const key = feature.properties.id; + const cached = FeatureReviews._featureReviewsCache[key]; if (cached !== undefined) { - return cached + return cached; } - const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options) - FeatureReviews._featureReviewsCache[key] = featureReviews - return featureReviews + const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options); + FeatureReviews._featureReviewsCache[key] = featureReviews; + return featureReviews; } /** @@ -155,15 +174,15 @@ export default class FeatureReviews { public async createReview(review: Omit): Promise { const r: Review = { sub: this.subjectUri.data, - ...review, - } - const keypair: CryptoKeyPair = this._identity.keypair.data - console.log(r) - const jwt = await MangroveReviews.signReview(keypair, r) - console.log("Signed:", jwt) - await MangroveReviews.submitReview(jwt) - this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) }) - this._reviews.ping() + ...review + }; + const keypair: CryptoKeyPair = this._identity.keypair.data; + console.log(r); + const jwt = await MangroveReviews.signReview(keypair, r); + console.log("Signed:", jwt); + await MangroveReviews.submitReview(jwt); + this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) }); + this._reviews.ping(); } /** @@ -172,46 +191,48 @@ export default class FeatureReviews { * @private */ private addReviews(reviews: { payload: Review; kid: string }[]) { - const self = this - const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion)) + const self = this; + const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion)); - let hasNew = false + let hasNew = false; for (const reviewData of reviews) { - const review = reviewData.payload + const review = reviewData.payload; try { - const url = new URL(review.sub) - console.log("URL is", url) + const url = new URL(review.sub); + console.log("URL is", url); if (url.protocol === "geo:") { const coordinate = <[number, number]>( url.pathname.split(",").map((n) => Number(n)) - ) + ); const distance = GeoOperations.distanceBetween( [this._lat, this._lon], coordinate - ) + ); if (distance > this._uncertainty) { - continue + continue; } } } catch (e) { - console.warn(e) + console.warn(e); } - const key = review.rating + " " + review.opinion + const key = review.rating + " " + review.opinion; if (alreadyKnown.has(key)) { - continue + continue; } self._reviews.data.push({ ...review, madeByLoggedInUser: this._identity.key_id.map((user_key_id) => { - return reviewData.kid === user_key_id - }), - }) - hasNew = true + return reviewData.kid === user_key_id; + }) + }); + hasNew = true; } if (hasNew) { - self._reviews.ping() + self._reviews.data.sort((a, b) => b.iat - a.iat) // Sort with most recent first + + self._reviews.ping(); } } @@ -224,13 +245,13 @@ export default class FeatureReviews { private ConstructSubjectUri(dontEncodeName: boolean = false): Store { // https://www.rfc-editor.org/rfc/rfc5870#section-3.4.2 // `u` stands for `uncertainty`, https://www.rfc-editor.org/rfc/rfc5870#section-3.4.3 - const self = this - return this._name.map(function (name) { - let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}` + const self = this; + return this._name.map(function(name) { + let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}`; if (name) { - uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name)) + uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name)); } - return uri - }) + return uri; + }); } } diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index b6f13012b..1adc59b5a 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -1,58 +1,62 @@ -import LayoutConfig from "./ThemeConfig/LayoutConfig"; -import { SpecialVisualizationState } from "../UI/SpecialVisualization"; -import { Changes } from "../Logic/Osm/Changes"; -import { Store, UIEventSource } from "../Logic/UIEventSource"; -import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"; -import { OsmConnection } from "../Logic/Osm/OsmConnection"; -import { ExportableMap, MapProperties } from "./MapProperties"; -import LayerState from "../Logic/State/LayerState"; -import { Feature, Point, Polygon } from "geojson"; -import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; -import { Map as MlMap } from "maplibre-gl"; -import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"; -import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"; -import { GeoLocationState } from "../Logic/State/GeoLocationState"; -import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; -import { QueryParameters } from "../Logic/Web/QueryParameters"; -import UserRelatedState from "../Logic/State/UserRelatedState"; -import LayerConfig from "./ThemeConfig/LayerConfig"; -import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"; -import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers"; -import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"; -import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; -import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"; -import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; -import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"; -import ShowDataLayer from "../UI/Map/ShowDataLayer"; -import TitleHandler from "../Logic/Actors/TitleHandler"; -import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"; -import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"; -import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater"; -import { BBox } from "../Logic/BBox"; -import Constants from "./Constants"; -import Hotkeys from "../UI/Base/Hotkeys"; -import Translations from "../UI/i18n/Translations"; -import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"; -import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"; -import { MenuState } from "./MenuState"; -import MetaTagging from "../Logic/MetaTagging"; -import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"; +import LayoutConfig from "./ThemeConfig/LayoutConfig" +import { SpecialVisualizationState } from "../UI/SpecialVisualization" +import { Changes } from "../Logic/Osm/Changes" +import { Store, UIEventSource } from "../Logic/UIEventSource" import { - NewGeometryFromChangesFeatureSource -} from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"; -import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; -import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer"; -import { Utils } from "../Utils"; -import { EliCategory } from "./RasterLayerProperties"; -import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"; -import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"; -import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"; -import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"; -import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector"; -import FilteredLayer from "./FilteredLayer"; -import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"; -import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"; -import { Imgur } from "../Logic/ImageProviders/Imgur"; + FeatureSource, + IndexedFeatureSource, + WritableFeatureSource, +} from "../Logic/FeatureSource/FeatureSource" +import { OsmConnection } from "../Logic/Osm/OsmConnection" +import { ExportableMap, MapProperties } from "./MapProperties" +import LayerState from "../Logic/State/LayerState" +import { Feature, Point, Polygon } from "geojson" +import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" +import { Map as MlMap } from "maplibre-gl" +import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning" +import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor" +import { GeoLocationState } from "../Logic/State/GeoLocationState" +import FeatureSwitchState from "../Logic/State/FeatureSwitchState" +import { QueryParameters } from "../Logic/Web/QueryParameters" +import UserRelatedState from "../Logic/State/UserRelatedState" +import LayerConfig from "./ThemeConfig/LayerConfig" +import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" +import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers" +import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" +import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" +import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore" +import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter" +import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource" +import ShowDataLayer from "../UI/Map/ShowDataLayer" +import TitleHandler from "../Logic/Actors/TitleHandler" +import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor" +import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader" +import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater" +import { BBox } from "../Logic/BBox" +import Constants from "./Constants" +import Hotkeys from "../UI/Base/Hotkeys" +import Translations from "../UI/i18n/Translations" +import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" +import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource" +import { MenuState } from "./MenuState" +import MetaTagging from "../Logic/MetaTagging" +import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator" +import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource" +import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" +import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer" +import { Utils } from "../Utils" +import { EliCategory } from "./RasterLayerProperties" +import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter" +import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" +import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" +import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" +import NoElementsInViewDetector, { + FeatureViewState, +} from "../Logic/Actors/NoElementsInViewDetector" +import FilteredLayer from "./FilteredLayer" +import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector" +import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" +import { Imgur } from "../Logic/ImageProviders/Imgur" /** * @@ -63,71 +67,71 @@ import { Imgur } from "../Logic/ImageProviders/Imgur"; * It ties up all the needed elements and starts some actors. */ export default class ThemeViewState implements SpecialVisualizationState { - readonly layout: LayoutConfig; - readonly map: UIEventSource; - readonly changes: Changes; - readonly featureSwitches: FeatureSwitchState; - readonly featureSwitchIsTesting: Store; - readonly featureSwitchUserbadge: Store; + readonly layout: LayoutConfig + readonly map: UIEventSource + readonly changes: Changes + readonly featureSwitches: FeatureSwitchState + readonly featureSwitchIsTesting: Store + readonly featureSwitchUserbadge: Store - readonly featureProperties: FeaturePropertiesStore; + readonly featureProperties: FeaturePropertiesStore - readonly osmConnection: OsmConnection; - readonly selectedElement: UIEventSource; - readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>; - readonly mapProperties: MapProperties & ExportableMap; - readonly osmObjectDownloader: OsmObjectDownloader; + readonly osmConnection: OsmConnection + readonly selectedElement: UIEventSource + readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> + readonly mapProperties: MapProperties & ExportableMap + readonly osmObjectDownloader: OsmObjectDownloader - readonly dataIsLoading: Store; + readonly dataIsLoading: Store /** * Indicates if there is _some_ data in view, even if it is not shown due to the filters */ - readonly hasDataInView: Store; + readonly hasDataInView: Store - readonly guistate: MenuState; - readonly fullNodeDatabase?: FullNodeDatabaseSource; + readonly guistate: MenuState + readonly fullNodeDatabase?: FullNodeDatabaseSource - readonly historicalUserLocations: WritableFeatureSource>; - readonly indexedFeatures: IndexedFeatureSource & LayoutSource; - readonly currentView: FeatureSource>; - readonly featuresInView: FeatureSource; - readonly newFeatures: WritableFeatureSource; - readonly layerState: LayerState; - readonly perLayer: ReadonlyMap; - readonly perLayerFiltered: ReadonlyMap; + readonly historicalUserLocations: WritableFeatureSource> + readonly indexedFeatures: IndexedFeatureSource & LayoutSource + readonly currentView: FeatureSource> + readonly featuresInView: FeatureSource + readonly newFeatures: WritableFeatureSource + readonly layerState: LayerState + readonly perLayer: ReadonlyMap + readonly perLayerFiltered: ReadonlyMap - readonly availableLayers: Store; - readonly selectedLayer: UIEventSource; - readonly userRelatedState: UserRelatedState; - readonly geolocation: GeoLocationHandler; + readonly availableLayers: Store + readonly selectedLayer: UIEventSource + readonly userRelatedState: UserRelatedState + readonly geolocation: GeoLocationHandler readonly imageUploadManager: ImageUploadManager - readonly lastClickObject: WritableFeatureSource; + readonly lastClickObject: WritableFeatureSource readonly overlayLayerStates: ReadonlyMap< string, { readonly isDisplayed: UIEventSource } - >; + > /** * All 'level'-tags that are available with the current features */ - readonly floors: Store; + readonly floors: Store constructor(layout: LayoutConfig) { - Utils.initDomPurify(); - this.layout = layout; - this.featureSwitches = new FeatureSwitchState(layout); + Utils.initDomPurify() + this.layout = layout + this.featureSwitches = new FeatureSwitchState(layout) this.guistate = new MenuState( this.featureSwitches.featureSwitchWelcomeMessage.data, layout.id - ); - this.map = new UIEventSource(undefined); - const initial = new InitialMapPositioning(layout); - this.mapProperties = new MapLibreAdaptor(this.map, initial); - const geolocationState = new GeoLocationState(); + ) + this.map = new UIEventSource(undefined) + const initial = new InitialMapPositioning(layout) + this.mapProperties = new MapLibreAdaptor(this.map, initial) + const geolocationState = new GeoLocationState() - this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting; - this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin; + this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting + this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin this.osmConnection = new OsmConnection({ dryRun: this.featureSwitches.featureSwitchIsTesting, @@ -137,68 +141,66 @@ export default class ThemeViewState implements SpecialVisualizationState { undefined, "Used to complete the login" ), - osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data - }); + osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data, + }) this.userRelatedState = new UserRelatedState( this.osmConnection, layout?.language, layout, this.featureSwitches, this.mapProperties - ); + ) this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => { - this.mapProperties.allowRotating.setData(fixated !== "yes"); - }); - this.selectedElement = new UIEventSource(undefined, "Selected element"); - this.selectedLayer = new UIEventSource(undefined, "Selected layer"); + this.mapProperties.allowRotating.setData(fixated !== "yes") + }) + this.selectedElement = new UIEventSource(undefined, "Selected element") + this.selectedLayer = new UIEventSource(undefined, "Selected layer") this.selectedElementAndLayer = this.selectedElement.mapD( (feature) => { - const layer = this.selectedLayer.data; + const layer = this.selectedLayer.data if (!layer) { - return undefined; + return undefined } - return { layer, feature }; + return { layer, feature } }, [this.selectedLayer] - ); + ) this.geolocation = new GeoLocationHandler( geolocationState, this.selectedElement, this.mapProperties, this.userRelatedState.gpsLocationHistoryRetentionTime - ); + ) - this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location); + this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location) - - const self = this; - this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id); + const self = this + this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id) { - const overlayLayerStates = new Map }>(); + const overlayLayerStates = new Map }>() for (const rasterInfo of this.layout.tileLayerSources) { const isDisplayed = QueryParameters.GetBooleanQueryParameter( "overlay-" + rasterInfo.id, rasterInfo.defaultState ?? true, "Wether or not overlayer layer " + rasterInfo.id + " is shown" - ); - const state = { isDisplayed }; - overlayLayerStates.set(rasterInfo.id, state); - new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state); + ) + const state = { isDisplayed } + overlayLayerStates.set(rasterInfo.id, state) + new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state) } - this.overlayLayerStates = overlayLayerStates; + this.overlayLayerStates = overlayLayerStates } - { /* Setup the layout source * A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too */ if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) { - this.fullNodeDatabase = new FullNodeDatabaseSource(); + this.fullNodeDatabase = new FullNodeDatabaseSource() } const layoutSource = new LayoutSource( @@ -208,49 +210,49 @@ export default class ThemeViewState implements SpecialVisualizationState { this.osmConnection.Backend(), (id) => self.layerState.filteredLayers.get(id).isDisplayed, this.fullNodeDatabase - ); + ) - this.indexedFeatures = layoutSource; + this.indexedFeatures = layoutSource - const empty = []; - let currentViewIndex = 0; + const empty = [] + let currentViewIndex = 0 this.currentView = new StaticFeatureSource( this.mapProperties.bounds.map((bbox) => { if (!bbox) { - return empty; + return empty } - currentViewIndex++; + currentViewIndex++ return [ bbox.asGeoJson({ zoom: this.mapProperties.zoom.data, ...this.mapProperties.location.data, - id: "current_view" - }) - ]; + id: "current_view", + }), + ] }) - ); - this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds); - this.dataIsLoading = layoutSource.isLoading; + ) + this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) + this.dataIsLoading = layoutSource.isLoading - const indexedElements = this.indexedFeatures; - this.featureProperties = new FeaturePropertiesStore(indexedElements); + const indexedElements = this.indexedFeatures + this.featureProperties = new FeaturePropertiesStore(indexedElements) this.changes = new Changes( { dryRun: this.featureSwitches.featureSwitchIsTesting, allElements: indexedElements, featurePropertiesStore: this.featureProperties, osmConnection: this.osmConnection, - historicalUserLocations: this.geolocation.historicalUserLocations + historicalUserLocations: this.geolocation.historicalUserLocations, }, layout?.isLeftRightSensitive() ?? false - ); - this.historicalUserLocations = this.geolocation.historicalUserLocations; + ) + this.historicalUserLocations = this.geolocation.historicalUserLocations this.newFeatures = new NewGeometryFromChangesFeatureSource( this.changes, indexedElements, this.featureProperties - ); - layoutSource.addSource(this.newFeatures); + ) + layoutSource.addSource(this.newFeatures) const perLayer = new PerLayerFeatureSourceSplitter( Array.from(this.layerState.filteredLayers.values()).filter( @@ -266,11 +268,11 @@ export default class ThemeViewState implements SpecialVisualizationState { features.length, "leftover features, such as", features[0].properties - ); - } + ) + }, } - ); - this.perLayer = perLayer.perLayer; + ) + this.perLayer = perLayer.perLayer } this.perLayer.forEach((fs) => { new SaveFeatureSourceToLocalStorage( @@ -280,74 +282,80 @@ export default class ThemeViewState implements SpecialVisualizationState { fs, this.featureProperties, fs.layer.layerDef.maxAgeOfCache - ); - }); + ) + }) this.floors = this.featuresInView.features.stabilized(500).map((features) => { if (!features) { - return []; + return [] } - const floors = new Set(); + const floors = new Set() for (const feature of features) { - const level = feature.properties["level"]; + let level = feature.properties["_level"] if (level) { - const levels = level.split(";"); + const levels = level.split(";") for (const l of levels) { - floors.add(l); + floors.add(l) } } else { - floors.add("0"); // '0' is the default and is thus _always_ present + floors.add("0") // '0' is the default and is thus _always_ present } } - const sorted = Array.from(floors); + const sorted = Array.from(floors) // Sort alphabetically first, to deal with floor "A", "B" and "C" - sorted.sort(); + sorted.sort() sorted.sort((a, b) => { // We use the laxer 'parseInt' to deal with floor '1A' - const na = parseInt(a); - const nb = parseInt(b); + const na = parseInt(a) + const nb = parseInt(b) if (isNaN(na) || isNaN(nb)) { - return 0; + return 0 } - return na - nb; - }); - sorted.reverse(/* new list, no side-effects */); - return sorted; - }); + return na - nb + }) + sorted.reverse(/* new list, no side-effects */) + return sorted + }) const lastClick = (this.lastClickObject = new LastClickFeatureSource( this.mapProperties.lastClickLocation, this.layout - )); + )) this.osmObjectDownloader = new OsmObjectDownloader( this.osmConnection.Backend(), this.changes - ); + ) - this.perLayerFiltered = this.showNormalDataOn(this.map); + this.perLayerFiltered = this.showNormalDataOn(this.map) - this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView; - this.imageUploadManager = new ImageUploadManager(layout, Imgur.singleton, this.featureProperties, this.osmConnection, this.changes) + this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView + this.imageUploadManager = new ImageUploadManager( + layout, + Imgur.singleton, + this.featureProperties, + this.osmConnection, + this.changes + ) - this.initActors(); - this.addLastClick(lastClick); - this.drawSpecialLayers(); - this.initHotkeys(); - this.miscSetup(); + this.initActors() + this.addLastClick(lastClick) + this.drawSpecialLayers() + this.initHotkeys() + this.miscSetup() if (!Utils.runningFromConsole) { - console.log("State setup completed", this); + console.log("State setup completed", this) } } public showNormalDataOn(map: Store): ReadonlyMap { - const filteringFeatureSource = new Map(); + const filteringFeatureSource = new Map() this.perLayer.forEach((fs, layerName) => { const doShowLayer = this.mapProperties.zoom.map( (z) => (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), [fs.layer.isDisplayed] - ); + ) if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) { /* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined) @@ -357,15 +365,15 @@ export default class ThemeViewState implements SpecialVisualizationState { * Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden. * However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer! * */ - return; + return } const filtered = new FilteringFeatureSource( fs.layer, fs, (id) => this.featureProperties.getStore(id), this.layerState.globalFilters - ); - filteringFeatureSource.set(layerName, filtered); + ) + filteringFeatureSource.set(layerName, filtered) new ShowDataLayer(map, { layer: fs.layer.layerDef, @@ -373,30 +381,30 @@ export default class ThemeViewState implements SpecialVisualizationState { doShowLayer, selectedElement: this.selectedElement, selectedLayer: this.selectedLayer, - fetchStore: (id) => this.featureProperties.getStore(id) - }); - }); - return filteringFeatureSource; + fetchStore: (id) => this.featureProperties.getStore(id), + }) + }) + return filteringFeatureSource } /** * Various small methods that need to be called */ private miscSetup() { - this.userRelatedState.markLayoutAsVisited(this.layout); + this.userRelatedState.markLayoutAsVisited(this.layout) this.selectedElement.addCallbackAndRunD((feature) => { // As soon as we have a selected element, we clear the selected element // This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature // The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear if (feature.properties.id === "last_click") { - return; + return } - this.lastClickObject.features.setData([]); - }); + this.lastClickObject.features.setData([]) + }) if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) { - Utils.LoadCustomCss(this.layout.customCss); + Utils.LoadCustomCss(this.layout.customCss) } } @@ -405,74 +413,74 @@ export default class ThemeViewState implements SpecialVisualizationState { { nomod: "Escape", onUp: true }, Translations.t.hotkeyDocumentation.closeSidebar, () => { - this.selectedElement.setData(undefined); - this.guistate.closeAll(); + this.selectedElement.setData(undefined) + this.guistate.closeAll() } - ); + ) Hotkeys.RegisterHotkey( { - nomod: "b" + nomod: "b", }, Translations.t.hotkeyDocumentation.openLayersPanel, () => { if (this.featureSwitches.featureSwitchFilter.data) { - this.guistate.openFilterView(); + this.guistate.openFilterView() } } - ); + ) Hotkeys.RegisterHotkey( { shift: "O" }, Translations.t.hotkeyDocumentation.selectMapnik, () => { - this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto); + this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto) } - ); + ) const setLayerCategory = (category: EliCategory) => { - const available = this.availableLayers.data; - const current = this.mapProperties.rasterLayer; + const available = this.availableLayers.data + const current = this.mapProperties.rasterLayer const best = RasterLayerUtils.SelectBestLayerAccordingTo( available, category, current.data - ); - console.log("Best layer for category", category, "is", best.properties.id); - current.setData(best); - }; + ) + console.log("Best layer for category", category, "is", best.properties.id) + current.setData(best) + } Hotkeys.RegisterHotkey( { nomod: "O" }, Translations.t.hotkeyDocumentation.selectOsmbasedmap, () => setLayerCategory("osmbasedmap") - ); + ) Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () => setLayerCategory("map") - ); + ) Hotkeys.RegisterHotkey( { nomod: "P" }, Translations.t.hotkeyDocumentation.selectAerial, () => setLayerCategory("photo") - ); + ) } private addLastClick(last_click: LastClickFeatureSource) { // The last_click gets a _very_ special treatment as it interacts with various parts - const last_click_layer = this.layerState.filteredLayers.get("last_click"); - this.featureProperties.trackFeatureSource(last_click); - this.indexedFeatures.addSource(last_click); + const last_click_layer = this.layerState.filteredLayers.get("last_click") + this.featureProperties.trackFeatureSource(last_click) + this.indexedFeatures.addSource(last_click) last_click.features.addCallbackAndRunD((features) => { if (this.selectedLayer.data?.id === "last_click") { // The last-click location moved, but we have selected the last click of the previous location // So, we update _after_ clearing the selection to make sure no stray data is sticking around - this.selectedElement.setData(undefined); - this.selectedElement.setData(features[0]); + this.selectedElement.setData(undefined) + this.selectedElement.setData(features[0]) } - }); + }) new ShowDataLayer(this.map, { features: new FilteringFeatureSource(last_click_layer, last_click), @@ -484,18 +492,18 @@ export default class ThemeViewState implements SpecialVisualizationState { if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) { this.map.data.flyTo({ zoom: Constants.minZoomLevelToAddNewPoint, - center: this.mapProperties.lastClickLocation.data - }); - return; + center: this.mapProperties.lastClickLocation.data, + }) + return } // We first clear the selection to make sure no weird state is around - this.selectedLayer.setData(undefined); - this.selectedElement.setData(undefined); + this.selectedLayer.setData(undefined) + this.selectedElement.setData(undefined) - this.selectedElement.setData(feature); - this.selectedLayer.setData(last_click_layer.layerDef); - } - }); + this.selectedElement.setData(feature) + this.selectedLayer.setData(last_click_layer.layerDef) + }, + }) } /** @@ -503,7 +511,7 @@ export default class ThemeViewState implements SpecialVisualizationState { */ private drawSpecialLayers() { type AddedByDefaultTypes = (typeof Constants.added_by_default)[number] - const empty = []; + const empty = [] /** * A listing which maps the layerId onto the featureSource */ @@ -523,21 +531,21 @@ export default class ThemeViewState implements SpecialVisualizationState { bbox === undefined ? empty : [bbox.asGeoJson({ id: "range" })] ) ), - current_view: this.currentView - }; + current_view: this.currentView, + } if (this.layout?.lockLocation) { - const bbox = new BBox(this.layout.lockLocation); - this.mapProperties.maxbounds.setData(bbox); + const bbox = new BBox(this.layout.lockLocation) + this.mapProperties.maxbounds.setData(bbox) ShowDataLayer.showRange( this.map, new StaticFeatureSource([bbox.asGeoJson({})]), this.featureSwitches.featureSwitchIsTesting - ); + ) } - const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view"); + const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view") if (currentViewLayer?.tagRenderings?.length > 0) { - const params = MetaTagging.createExtraFuncParams(this); - this.featureProperties.trackFeatureSource(specialLayers.current_view); + const params = MetaTagging.createExtraFuncParams(this) + this.featureProperties.trackFeatureSource(specialLayers.current_view) specialLayers.current_view.features.addCallbackAndRunD((features) => { MetaTagging.addMetatags( features, @@ -546,37 +554,37 @@ export default class ThemeViewState implements SpecialVisualizationState { this.layout, this.osmObjectDownloader, this.featureProperties - ); - }); + ) + }) } - const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range"); + const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range") - const rangeIsDisplayed = rangeFLayer?.isDisplayed; + const rangeIsDisplayed = rangeFLayer?.isDisplayed if ( !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef)) ) { - rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true); + rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true) } this.layerState.filteredLayers.forEach((flayer) => { - const id = flayer.layerDef.id; - const features: FeatureSource = specialLayers[id]; + const id = flayer.layerDef.id + const features: FeatureSource = specialLayers[id] if (features === undefined) { - return; + return } - this.featureProperties.trackFeatureSource(features); + this.featureProperties.trackFeatureSource(features) // this.indexedFeatures.addSource(features) new ShowDataLayer(this.map, { features, doShowLayer: flayer.isDisplayed, layer: flayer.layerDef, selectedElement: this.selectedElement, - selectedLayer: this.selectedLayer - }); - }); + selectedLayer: this.selectedLayer, + }) + }) } /** @@ -585,30 +593,35 @@ export default class ThemeViewState implements SpecialVisualizationState { private initActors() { // Unselect the selected element if it is panned out of view this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => { - const selected = this.selectedElement.data; + const selected = this.selectedElement.data if (selected === undefined) { - return; + return } - const bbox = BBox.get(selected); + const bbox = BBox.get(selected) if (!bbox.overlapsWith(bounds)) { - this.selectedElement.setData(undefined); + this.selectedElement.setData(undefined) } - }); + }) this.selectedElement.addCallback((selected) => { if (selected === undefined) { // We did _unselect_ an item - we always remove the lastclick-object - this.lastClickObject.features.setData([]); - this.selectedLayer.setData(undefined); + this.lastClickObject.features.setData([]) + this.selectedLayer.setData(undefined) } - }); - new ThemeViewStateHashActor(this); - new MetaTagging(this); - new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this); - new ChangeToElementsActor(this.changes, this.featureProperties); - new PendingChangesUploader(this.changes, this.selectedElement); - new SelectedElementTagsUpdater(this); - new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers); - new PreferredRasterLayerSelector(this.mapProperties.rasterLayer, this.availableLayers, this.featureSwitches.backgroundLayerId, this.userRelatedState.preferredBackgroundLayer) + }) + new ThemeViewStateHashActor(this) + new MetaTagging(this) + new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this) + new ChangeToElementsActor(this.changes, this.featureProperties) + new PendingChangesUploader(this.changes, this.selectedElement) + new SelectedElementTagsUpdater(this) + new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers) + new PreferredRasterLayerSelector( + this.mapProperties.rasterLayer, + this.availableLayers, + this.featureSwitches.backgroundLayerId, + this.userRelatedState.preferredBackgroundLayer + ) } } diff --git a/src/UI/Base/Checkbox.svelte b/src/UI/Base/Checkbox.svelte index 010992df9..92de427ac 100644 --- a/src/UI/Base/Checkbox.svelte +++ b/src/UI/Base/Checkbox.svelte @@ -1,12 +1,14 @@ - - + diff --git a/src/UI/BigComponents/Filterview.svelte b/src/UI/BigComponents/Filterview.svelte index e4f3e234d..7ed9c26c4 100644 --- a/src/UI/BigComponents/Filterview.svelte +++ b/src/UI/BigComponents/Filterview.svelte @@ -55,27 +55,26 @@ {#if filteredLayer.layerDef.name}
- + {/if} + {#if $isDisplayed && filteredLayer.layerDef.filters?.length > 0}
@@ -83,10 +82,9 @@
{#if filter.options.length === 1 && filter.options[0].fields.length === 0} - + + {filter.options[0].question} + {/if} {#if filter.options.length === 1 && filter.options[0].fields.length > 0} diff --git a/src/UI/BigComponents/SelectedElementTitle.svelte b/src/UI/BigComponents/SelectedElementTitle.svelte index 01c0bbaf9..600087dc7 100644 --- a/src/UI/BigComponents/SelectedElementTitle.svelte +++ b/src/UI/BigComponents/SelectedElementTitle.svelte @@ -46,7 +46,7 @@ > {#each layer.titleIcons as titleIconConfig} {#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ..._metatags, ..._tags } ) ?? true) && titleIconConfig.IsKnown(_tags)} -
+
- + - {:else if $geopermission !== "denied"} + {:else if $geopermission === "denied"} + {:else } + + {/if}
diff --git a/src/UI/Reviews/AllReviews.svelte b/src/UI/Reviews/AllReviews.svelte new file mode 100644 index 000000000..bdc4a2222 --- /dev/null +++ b/src/UI/Reviews/AllReviews.svelte @@ -0,0 +1,47 @@ + + +
+ {#if _reviews.length > 1} + + {/if} + {#if _reviews.length > 0} + {#each _reviews as review} + + {/each} + {:else} + + {/if} +
+ + +
+
diff --git a/src/UI/Reviews/ReviewElement.ts b/src/UI/Reviews/ReviewElement.ts deleted file mode 100644 index efdf42fe8..000000000 --- a/src/UI/Reviews/ReviewElement.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Combine from "../Base/Combine" -import Translations from "../i18n/Translations" -import SingleReview from "./SingleReview" -import BaseUIElement from "../BaseUIElement" -import Img from "../Base/Img" -import { VariableUiElement } from "../Base/VariableUIElement" -import Link from "../Base/Link" -import FeatureReviews from "../../Logic/Web/MangroveReviews" - -/** - * Shows the reviews and scoring base on mangrove.reviews - * The middle element is some other component shown in the middle, e.g. the review input element - */ -export default class ReviewElement extends VariableUiElement { - constructor(reviews: FeatureReviews, middleElement: BaseUIElement) { - super( - reviews.reviews.map( - (revs) => { - const elements = [] - revs.sort((a, b) => b.iat - a.iat) // Sort with most recent first - const avg = - revs.map((review) => review.rating).reduce((a, b) => a + b, 0) / revs.length - elements.push( - new Combine([ - SingleReview.GenStars(avg), - new Link( - revs.length === 1 - ? Translations.t.reviews.title_singular.Clone() - : Translations.t.reviews.title.Subs({ - count: "" + revs.length, - }), - `https://mangrove.reviews/search?sub=${encodeURIComponent( - reviews.subjectUri.data - )}`, - true - ), - ]).SetClass("font-2xl flex justify-between items-center pl-2 pr-2") - ) - - elements.push(middleElement) - - elements.push(...revs.map((review) => new SingleReview(review))) - elements.push( - new Combine([ - Translations.t.reviews.attribution.Clone(), - new Img("./assets/mangrove_logo.png"), - ]).SetClass("review-attribution") - ) - - return new Combine(elements).SetClass("block") - }, - [reviews.subjectUri] - ) - ) - } -} diff --git a/src/UI/Reviews/ReviewForm.svelte b/src/UI/Reviews/ReviewForm.svelte new file mode 100644 index 000000000..7949a457d --- /dev/null +++ b/src/UI/Reviews/ReviewForm.svelte @@ -0,0 +1,97 @@ + +{#if _state === "done"} + +{:else if _state === "saving"} + + + +{:else} +
+
+ +
+ {confirmedScore = e.detail.score}} on:hover={e => {score = e.detail.score}} + on:mouseout={e => {score = null}} score={score ?? confirmedScore ?? 0} + starSize="w-8 h-8"> + + {#if confirmedScore !== undefined} + +