forked from MapComplete/MapComplete
Accessibility: improve keyboard only flow (see #1181); remove some legacy use of Svelte
This commit is contained in:
parent
d1a6c11513
commit
4ee83cfe5c
35 changed files with 613 additions and 683 deletions
|
@ -351,6 +351,7 @@
|
|||
"if": {
|
||||
"and": [
|
||||
"seasonal!=no",
|
||||
"seasonal~*",
|
||||
{
|
||||
"or": [
|
||||
{
|
||||
|
|
|
@ -1,33 +1,13 @@
|
|||
{
|
||||
"id": "mapcomplete-changes",
|
||||
"title": {
|
||||
"en": "Changes made with MapComplete",
|
||||
"ca": "Canvis fets amb MapComplete",
|
||||
"cs": "Změny provedené pomocí MapComplete",
|
||||
"de": "Mit MapComplete erstellte Änderungen",
|
||||
"es": "Cambios realizados con MapComplete",
|
||||
"fr": "Changements faits avec MapComplete",
|
||||
"nl": "Wijzigingen gemaakt met MapComplete",
|
||||
"pl": "Zmiany wprowadzone za pomocą MapComplete"
|
||||
"en": "Changes made with MapComplete"
|
||||
},
|
||||
"shortDescription": {
|
||||
"en": "Show changes made with MapComplete",
|
||||
"ca": "Mostra els canvis fets amb MapComplete",
|
||||
"cs": "Zobrazení změn provedených pomocí nástroje MapComplete",
|
||||
"de": "Mit MapComplete erstellte Änderungen anzeigen",
|
||||
"es": "Mostrar cambios realizados con MapComplete",
|
||||
"nl": "Toon wijzigingen gemaakt met MapComplete",
|
||||
"pl": "Pokaż zmiany wprowadzone za pomocą MapComplete"
|
||||
"en": "Shows changes made by MapComplete"
|
||||
},
|
||||
"description": {
|
||||
"en": "This maps shows all the changes made with MapComplete",
|
||||
"ca": "Aquest mapa mostra tots els canvis fets amb MapComplete",
|
||||
"cs": "Tato mapa zobrazuje všechny změny provedené pomocí MapComplete",
|
||||
"de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen",
|
||||
"es": "Este mapa muestra todos los cambios realizados con MapComplete",
|
||||
"fr": "Cette carte montre tous les changements faits avec MapComplete",
|
||||
"nl": "Deze kaart toont alle wijzigingen die met MapComplete gemaakt werden",
|
||||
"pl": "Ta mapa pokazuje wszystkie zmiany wprowadzone za pomocą MapComplete"
|
||||
"en": "This maps shows all the changes made with MapComplete"
|
||||
},
|
||||
"icon": "./assets/svg/logo.svg",
|
||||
"hideFromOverview": true,
|
||||
|
@ -40,13 +20,7 @@
|
|||
{
|
||||
"id": "mapcomplete-changes",
|
||||
"name": {
|
||||
"en": "Changeset centers",
|
||||
"ca": "Centre del conjunt de canvis",
|
||||
"cs": "Centrum změn",
|
||||
"de": "Zentrum der Änderungssätze",
|
||||
"es": "Centro del conjunto de cambios",
|
||||
"nl": "Centerpunt van changeset",
|
||||
"pl": "Centra zmian"
|
||||
"en": "Changeset centers"
|
||||
},
|
||||
"minzoom": 0,
|
||||
"source": {
|
||||
|
@ -57,85 +31,41 @@
|
|||
},
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Changeset for {theme}",
|
||||
"ca": "Conjunt de canvis per a {theme}",
|
||||
"cs": "Změna pro {theme}",
|
||||
"de": "Änderungssatz für {theme}",
|
||||
"es": "Conjunto de cambios para {theme}",
|
||||
"fr": "Groupe de modifications pour {theme}",
|
||||
"pl": "Zestaw zmian dla {theme}"
|
||||
"en": "Changeset for {theme}"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"en": "Show all MapComplete changes",
|
||||
"ca": "Mostra tots els canvis de MapComplete",
|
||||
"cs": "Zobrazit všechny změny MapComplete",
|
||||
"de": "Alle MapComplete-Änderungen anzeigen",
|
||||
"es": "Mostrar todos los cambios de MapComplete",
|
||||
"nl": "Toon alle MapComplete wijzigingen",
|
||||
"pl": "Wyświetl wszystkie zmiany MapComplete"
|
||||
"en": "Shows all MapComplete changes"
|
||||
},
|
||||
"tagRenderings": [
|
||||
{
|
||||
"id": "show_changeset_id",
|
||||
"render": {
|
||||
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"ca": "Conjunt de canvi <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"cs": "Změny <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"de": "Änderungssatz <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"es": "Conjunto de cambios <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"fr": "Groupe de modifications <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"pl": "Zestaw zmian <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
|
||||
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "contributor",
|
||||
"question": {
|
||||
"en": "Which contributor made this change?",
|
||||
"ca": "Quin col·laborador va fer aquest canvi?",
|
||||
"cs": "Který přispěvatel tuto změnu provedl?",
|
||||
"de": "Wer hat diese Änderung vorgenommen?",
|
||||
"es": "¿Qué contribuidor hizo este cambio?",
|
||||
"fr": "Quel contributeur a fait cette modification ?",
|
||||
"nl": "Welke bijdrager maakte deze wijziging?",
|
||||
"pl": "Który współautor dokonał tej zmiany?"
|
||||
"en": "What contributor did make this change?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "user"
|
||||
},
|
||||
"render": {
|
||||
"en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"ca": "Canvi fet per <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"cs": "Změna provedená <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"de": "Änderung von <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"es": "Cambio realizado por <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"fr": "Modification faite par <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"nl": "Wijziging gemaakt door <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"pl": "Zmiana dokonana przez <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
|
||||
"en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "theme-id",
|
||||
"question": {
|
||||
"en": "What theme was used to make this change?",
|
||||
"ca": "Quin tema es va utilitzar per fer aquest canvi?",
|
||||
"cs": "Jaké téma bylo použito k provedení této změny?",
|
||||
"de": "Welches Thema wurde für diese Änderung verwendet?",
|
||||
"es": "¿Qué tema se utilizó para realizar este cambio?",
|
||||
"fr": "Quel thème a été utilisé pour faire cette modification ?",
|
||||
"pl": "Jakiego tematu użyto do wprowadzenia tej zmiany?"
|
||||
"en": "What theme was used to make this change?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "theme"
|
||||
},
|
||||
"render": {
|
||||
"en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
|
||||
"ca": "Canvi amb el tema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
|
||||
"cs": "Změna s motivem <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
|
||||
"de": "Geändert mit Thema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
|
||||
"es": "Cambio con tema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
|
||||
"fr": "Modifié avec le thème <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
|
||||
"pl": "Zmiana za pomocą motywu <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
|
||||
"en": "Change with theme <a href='https://mapcomplete.org/{theme}'>{theme}</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -144,45 +74,19 @@
|
|||
"key": "locale"
|
||||
},
|
||||
"question": {
|
||||
"en": "What locale (language) was this change made in?",
|
||||
"ca": "Amb quina configuració regional (idioma) s'ha fet aquest canvi?",
|
||||
"cs": "V jakém národním prostředí (jazyce) byla tato změna provedena?",
|
||||
"de": "In welcher Benutzersprache wurde diese Änderung vorgenommen?",
|
||||
"es": "¿En qué configuración regional (idioma) se realizó este cambio?",
|
||||
"fr": "En quelle langue est-ce que ce changement a été fait ?",
|
||||
"nl": "In welke locale (taal) werd deze wijziging gemaakt?",
|
||||
"pl": "W jakim języku wprowadzono tę zmianę?"
|
||||
"en": "What locale (language) was this change made in?"
|
||||
},
|
||||
"render": {
|
||||
"en": "User locale is {locale}",
|
||||
"ca": "La configuració regional de l'usuari és {locale}",
|
||||
"cs": "Uživatelské prostředí je {locale}",
|
||||
"de": "Benutzersprache {locale}",
|
||||
"es": "La configuración regional del usuario es {locale}",
|
||||
"nl": "De gebruikerstaal is {locale}",
|
||||
"pl": "Ustawienia regionalne użytkownika to {locale}"
|
||||
"en": "User locale is {locale}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "host",
|
||||
"render": {
|
||||
"en": "Change made with <a href='{host}'>{host}</a>",
|
||||
"ca": "Canviat fet amb <a href='{host}'>{host}</a>",
|
||||
"cs": "Změna provedená pomocí <a href='{host}'>{host}</a>",
|
||||
"de": "Geändert über <a href='{host}'>{host}</a>",
|
||||
"es": "Cambio realizado con <a href='{host}'>{host}</a>",
|
||||
"fr": "Modification faite avec <a href='{host}'>{host}</a>",
|
||||
"nl": "Wijziging gemaakt met <a href='{host}'>{host}</a>",
|
||||
"pl": "Zmiana dokonana w <a href='{host}'>{host}</a>"
|
||||
"en": "Change with with <a href='{host}'>{host}</a>"
|
||||
},
|
||||
"question": {
|
||||
"en": "What host (website) was this change made with?",
|
||||
"ca": "Amb quin amfitrió (lloc web) es va fer aquest canvi?",
|
||||
"cs": "U jakého hostitele (webové stránky) byla tato změna provedena?",
|
||||
"de": "Über welchen Host (Webseite) wurde diese Änderung vorgenommen?",
|
||||
"es": "¿Con qué host (página web) se realizó este cambio?",
|
||||
"nl": "Met welke host (website) werd deze wijziging gemaakt?",
|
||||
"pl": "Na jakim hoście (stronie internetowej) dokonano tej zmiany?"
|
||||
"en": "What host (website) was this change made with?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "host"
|
||||
|
@ -203,22 +107,10 @@
|
|||
{
|
||||
"id": "version",
|
||||
"question": {
|
||||
"en": "What version of MapComplete was used to make this change?",
|
||||
"ca": "Quina versió de MapComplete es va utilitzar per fer aquest canvi?",
|
||||
"cs": "Jaká verze aplikace MapComplete byla použita k provedení této změny?",
|
||||
"de": "Mit welcher Version von MapComplete wurde diese Änderung gemacht?",
|
||||
"es": "¿Qué versión de MapComplete se usó para realizar este cambio?",
|
||||
"fr": "Quelle version de MapComplete a été utilisée pour faire cette modification ?",
|
||||
"pl": "Która wersja MapComplete została wykorzystana, aby zrobić tę zmianę?"
|
||||
"en": "What version of MapComplete was used to make this change?"
|
||||
},
|
||||
"render": {
|
||||
"en": "Made with {editor}",
|
||||
"ca": "Fet amb {editor}",
|
||||
"cs": "Vyrobeno pomocí {editor}",
|
||||
"de": "Erstellt mit {editor}",
|
||||
"es": "Realizado con {editor}",
|
||||
"fr": "Fait avec {editor}",
|
||||
"pl": "Zrobione za pomocą {editor}"
|
||||
"en": "Made with {editor}"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "editor"
|
||||
|
@ -568,13 +460,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "Theme name contains {search}",
|
||||
"ca": "El nom del tema conté {search}",
|
||||
"cs": "Název motivu obsahuje {search}",
|
||||
"de": "Themenname enthält {search}",
|
||||
"es": "El nombre del tema contiene {search}",
|
||||
"nl": "Themenaam bevat {search}",
|
||||
"pl": "Nazwa tematu zawiera {search}"
|
||||
"en": "Themename contains {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -590,7 +476,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "Theme name does <b>not</b> contain {search}"
|
||||
"en": "Themename does <b>not</b> contain {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -606,13 +492,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "Made by contributor {search}",
|
||||
"ca": "Fet pel col·laborador {search}",
|
||||
"cs": "Vytvořil přispěvatel {search}",
|
||||
"de": "Erstellt von {search}",
|
||||
"es": "Hecho por el colaborador {search}",
|
||||
"nl": "Gemaakt door bijdrager {search}",
|
||||
"pl": "Wykonane przez współautora {search}"
|
||||
"en": "Made by contributor {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -628,13 +508,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "<b>Not</b> made by contributor {search}",
|
||||
"ca": "<b>No</b> fet pel col·laborador {search}",
|
||||
"cs": "<b>Není</b> vytvořeno přispěvatelem {search}",
|
||||
"de": "<b>Nicht</b> erstellt von {search}",
|
||||
"es": "<b>No</b> hecho por el colaborador {search}",
|
||||
"nl": "<b>Niet</b> gemaakt door bijdrager {search}",
|
||||
"pl": "<b>Nie</b> wykonane przez współautora {search}"
|
||||
"en": "<b>Not</b> made by contributor {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -651,13 +525,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "Made before {search}",
|
||||
"ca": "Fet abans de {search}",
|
||||
"cs": "Vytvořeno před {search}",
|
||||
"de": "Erstellt vor {search}",
|
||||
"es": "Hecho antes de {search}",
|
||||
"nl": "Gemaakt voor {search}",
|
||||
"pl": "Stworzone przed {search}"
|
||||
"en": "Made before {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -674,13 +542,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "Made after {search}",
|
||||
"ca": "Fet després de {search}",
|
||||
"cs": "Vytvořeno po {search}",
|
||||
"de": "Erstellt nach {search}",
|
||||
"es": "Hecho después de {search}",
|
||||
"nl": "Gemaakt na {search}",
|
||||
"pl": "Stworzone po {search}"
|
||||
"en": "Made after {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -696,14 +558,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "User language (iso-code) {search}",
|
||||
"ca": "Idioma de l'usuari (codi iso) {search}",
|
||||
"cs": "Jazyk uživatele (iso-kód) {search}",
|
||||
"de": "Benutzersprache (ISO-Code) {search}",
|
||||
"es": "Use idioma (ISO-code) {search}",
|
||||
"fr": "Langage utilisateur (code-ISO) {search}",
|
||||
"nl": "De taal van de bijdrager is {search}",
|
||||
"pl": "Język użytkownika (kod iso) {search}"
|
||||
"en": "User language (iso-code) {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -719,13 +574,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "Made with host {search}",
|
||||
"ca": "Fet amb l'amfitrió {search}",
|
||||
"cs": "Vytvořeno pomocí hostitele {search}",
|
||||
"de": "Erstellt mit Host {search}",
|
||||
"es": "Hecho con el host {search}",
|
||||
"nl": "Gemaakt met host {search}",
|
||||
"pl": "Wykonane z hostem {search}"
|
||||
"en": "Made with host {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -736,14 +585,7 @@
|
|||
{
|
||||
"osmTags": "add-image>0",
|
||||
"question": {
|
||||
"en": "Changeset added at least one image",
|
||||
"ca": "El conjunt de canvis ha afegit almenys una imatge",
|
||||
"cs": "Sada změn přidala alespoň jeden obrázek",
|
||||
"de": "Im Änderungssatz wurde mindestens ein Bild hinzugefügt",
|
||||
"es": "El conjunto de cambios ha añadido al menos una imagen",
|
||||
"fr": "Le groupe de modifications a ajouté au moins une image",
|
||||
"nl": "Changeset bevat minstens één afbeelding",
|
||||
"pl": "Zestaw zmian dodał co najmniej jedno zdjęcie"
|
||||
"en": "Changeset added at least one image"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -754,7 +596,7 @@
|
|||
{
|
||||
"osmTags": "theme!=grb",
|
||||
"question": {
|
||||
"en": "Made with host {search}"
|
||||
"en": "Exclude GRB theme"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -765,7 +607,7 @@
|
|||
{
|
||||
"osmTags": "theme!=etymology",
|
||||
"question": {
|
||||
"en": "Changeset added at least one image"
|
||||
"en": "Exclude etymology theme"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -780,13 +622,7 @@
|
|||
{
|
||||
"id": "link_to_more",
|
||||
"render": {
|
||||
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>",
|
||||
"ca": "Es pot trobar més estadística <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>aquí</a>",
|
||||
"cs": "Další statistiky najdete <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>",
|
||||
"de": "Mehr Statistiken gibt es <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a>",
|
||||
"es": "Puede encontrar más estadísticas <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>aquí</a>",
|
||||
"fr": "D'autres statistiques sont disponibles <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>ici</a>",
|
||||
"pl": "Więcej statystyk można znaleźć <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>tutaj</a>"
|
||||
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mapcomplete",
|
||||
"version": "0.36.1",
|
||||
"version": "0.36.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mapcomplete",
|
||||
"version": "0.36.1",
|
||||
"version": "0.36.2",
|
||||
"repository": "https://github.com/pietervdvn/MapComplete",
|
||||
"description": "A small website to edit OSM easily",
|
||||
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
|
||||
|
|
|
@ -777,10 +777,6 @@ video {
|
|||
float: left;
|
||||
}
|
||||
|
||||
.m-8 {
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.m-4 {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
@ -793,6 +789,10 @@ video {
|
|||
margin: 0px;
|
||||
}
|
||||
|
||||
.m-8 {
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.m-2 {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
@ -1188,6 +1188,10 @@ video {
|
|||
max-height: 6rem;
|
||||
}
|
||||
|
||||
.max-h-64 {
|
||||
max-height: 16rem;
|
||||
}
|
||||
|
||||
.max-h-7 {
|
||||
max-height: 1.75rem;
|
||||
}
|
||||
|
@ -1266,6 +1270,10 @@ video {
|
|||
width: 3.5rem;
|
||||
}
|
||||
|
||||
.w-auto {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.w-5 {
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
@ -1283,10 +1291,6 @@ video {
|
|||
width: 12rem;
|
||||
}
|
||||
|
||||
.w-auto {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.max-w-full {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
@ -1685,6 +1689,10 @@ video {
|
|||
border-style: dotted;
|
||||
}
|
||||
|
||||
.border-none {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
.border-black {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(0 0 0 / var(--tw-border-opacity));
|
||||
|
@ -2234,6 +2242,11 @@ body {
|
|||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.focusable {
|
||||
/* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */
|
||||
border: 1px solid red
|
||||
}
|
||||
|
||||
svg,
|
||||
img {
|
||||
box-sizing: content-box;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { RegexTag } from "../src/Logic/Tags/RegexTag"
|
|||
import { ImmutableStore } from "../src/Logic/UIEventSource"
|
||||
import { BBox } from "../src/Logic/BBox"
|
||||
import * as fs from "fs"
|
||||
import { writeFileSync } from "fs"
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
|
||||
import { Feature } from "geojson"
|
||||
import ScriptUtils from "./ScriptUtils"
|
||||
import { Imgur } from "../src/Logic/ImageProviders/Imgur"
|
||||
|
@ -181,6 +181,32 @@ export default class GenerateImageAnalysis extends Script {
|
|||
}
|
||||
}
|
||||
|
||||
async downloadViews(datapath: string): Promise<void> {
|
||||
const { allImages, imageSource } = this.loadImageUrls(datapath)
|
||||
console.log("Detected", allImages.size, "images")
|
||||
const results: [string, number][] = []
|
||||
const today = new Date().toISOString().substring(0, "YYYY-MM-DD".length)
|
||||
const viewDir = datapath + "/views_" + today
|
||||
if (!existsSync(viewDir)) {
|
||||
mkdirSync(viewDir)
|
||||
}
|
||||
|
||||
for (const image of Array.from(allImages)) {
|
||||
const cachedView = viewDir + "/" + image.replace(/\\/g, "_")
|
||||
let attribution: LicenseInfo
|
||||
if (existsSync(cachedView)) {
|
||||
attribution = JSON.parse(readFileSync(cachedView, "utf8"))
|
||||
} else {
|
||||
attribution = await Imgur.singleton.DownloadAttribution(image)
|
||||
writeFileSync(cachedView, JSON.stringify(attribution))
|
||||
}
|
||||
results.push([image, attribution.views])
|
||||
}
|
||||
const targetpath = datapath + "/views.csv"
|
||||
console.log("Writing views to", targetpath)
|
||||
fs.writeFileSync(targetpath, results.map((r) => r.join(",")).join("\n"))
|
||||
}
|
||||
|
||||
async downloadImage(url: string, imagePath: string): Promise<boolean> {
|
||||
const filenameLong = url.replace(/[\/:.\-%]/g, "_") + ".jpg"
|
||||
const targetPathLong = imagePath + "/" + filenameLong
|
||||
|
@ -391,6 +417,7 @@ export default class GenerateImageAnalysis extends Script {
|
|||
await this.downloadData(datapath, cached)
|
||||
|
||||
await this.downloadMetadata(datapath)
|
||||
await this.downloadViews(datapath)
|
||||
await this.downloadAllImages(datapath, imageBackupPath)
|
||||
this.analyze(datapath)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ interface TagsUpdaterState {
|
|||
osmObjectDownloader: OsmObjectDownloader
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
}
|
||||
|
||||
export default class SelectedElementTagsUpdater {
|
||||
private static readonly metatags = new Set([
|
||||
"timestamp",
|
||||
|
@ -42,6 +43,84 @@ export default class SelectedElementTagsUpdater {
|
|||
})
|
||||
}
|
||||
|
||||
public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) {
|
||||
try {
|
||||
const leftRightSensitive = state.layout.isLeftRightSensitive()
|
||||
|
||||
if (leftRightSensitive) {
|
||||
SimpleMetaTagger.removeBothTagging(latestTags)
|
||||
}
|
||||
|
||||
const pendingChanges = state.changes.pendingChanges.data
|
||||
.filter((change) => change.type + "/" + change.id === id)
|
||||
.filter((change) => change.tags !== undefined)
|
||||
|
||||
for (const pendingChange of pendingChanges) {
|
||||
const tagChanges = pendingChange.tags
|
||||
for (const tagChange of tagChanges) {
|
||||
const key = tagChange.k
|
||||
const v = tagChange.v
|
||||
if (v === undefined || v === "") {
|
||||
delete latestTags[key]
|
||||
} else {
|
||||
latestTags[key] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With the changes applied, we merge them onto the upstream object
|
||||
let somethingChanged = false
|
||||
const currentTagsSource = state.featureProperties.getStore(id)
|
||||
if (currentTagsSource === undefined) {
|
||||
console.warn("No tags store found for", id, "cannot update tags")
|
||||
return
|
||||
}
|
||||
const currentTags = currentTagsSource.data
|
||||
for (const key in latestTags) {
|
||||
let osmValue = latestTags[key]
|
||||
|
||||
if (typeof osmValue === "number") {
|
||||
osmValue = "" + osmValue
|
||||
}
|
||||
|
||||
const localValue = currentTags[key]
|
||||
if (localValue !== osmValue) {
|
||||
somethingChanged = true
|
||||
currentTags[key] = osmValue
|
||||
}
|
||||
}
|
||||
|
||||
for (const currentKey in currentTags) {
|
||||
if (currentKey.startsWith("_")) {
|
||||
continue
|
||||
}
|
||||
if (SelectedElementTagsUpdater.metatags.has(currentKey)) {
|
||||
continue
|
||||
}
|
||||
if (currentKey in latestTags) {
|
||||
continue
|
||||
}
|
||||
console.log("Removing key as deleted upstream", currentKey)
|
||||
delete currentTags[currentKey]
|
||||
somethingChanged = true
|
||||
}
|
||||
|
||||
if (somethingChanged) {
|
||||
console.log(
|
||||
"Detected upstream changes to the object " +
|
||||
id +
|
||||
" when opening it, updating..."
|
||||
)
|
||||
currentTagsSource.ping()
|
||||
} else {
|
||||
console.debug("Fetched latest tags for ", id, "but detected no changes")
|
||||
}
|
||||
return currentTags
|
||||
} catch (e) {
|
||||
console.error("Updating the tags of selected element ", id, "failed due to", e)
|
||||
}
|
||||
}
|
||||
|
||||
private installCallback(state: TagsUpdaterState) {
|
||||
state.selectedElement.addCallbackAndRunD(async (s) => {
|
||||
let id = s.properties?.id
|
||||
|
@ -90,77 +169,4 @@ export default class SelectedElementTagsUpdater {
|
|||
}
|
||||
})
|
||||
}
|
||||
public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) {
|
||||
try {
|
||||
const leftRightSensitive = state.layout.isLeftRightSensitive()
|
||||
|
||||
if (leftRightSensitive) {
|
||||
SimpleMetaTagger.removeBothTagging(latestTags)
|
||||
}
|
||||
|
||||
const pendingChanges = state.changes.pendingChanges.data
|
||||
.filter((change) => change.type + "/" + change.id === id)
|
||||
.filter((change) => change.tags !== undefined)
|
||||
|
||||
for (const pendingChange of pendingChanges) {
|
||||
const tagChanges = pendingChange.tags
|
||||
for (const tagChange of tagChanges) {
|
||||
const key = tagChange.k
|
||||
const v = tagChange.v
|
||||
if (v === undefined || v === "") {
|
||||
delete latestTags[key]
|
||||
} else {
|
||||
latestTags[key] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With the changes applied, we merge them onto the upstream object
|
||||
let somethingChanged = false
|
||||
const currentTagsSource = state.featureProperties.getStore(id)
|
||||
const currentTags = currentTagsSource.data
|
||||
for (const key in latestTags) {
|
||||
let osmValue = latestTags[key]
|
||||
|
||||
if (typeof osmValue === "number") {
|
||||
osmValue = "" + osmValue
|
||||
}
|
||||
|
||||
const localValue = currentTags[key]
|
||||
if (localValue !== osmValue) {
|
||||
somethingChanged = true
|
||||
currentTags[key] = osmValue
|
||||
}
|
||||
}
|
||||
|
||||
for (const currentKey in currentTags) {
|
||||
if (currentKey.startsWith("_")) {
|
||||
continue
|
||||
}
|
||||
if (SelectedElementTagsUpdater.metatags.has(currentKey)) {
|
||||
continue
|
||||
}
|
||||
if (currentKey in latestTags) {
|
||||
continue
|
||||
}
|
||||
console.log("Removing key as deleted upstream", currentKey)
|
||||
delete currentTags[currentKey]
|
||||
somethingChanged = true
|
||||
}
|
||||
|
||||
if (somethingChanged) {
|
||||
console.log(
|
||||
"Detected upstream changes to the object " +
|
||||
id +
|
||||
" when opening it, updating..."
|
||||
)
|
||||
currentTagsSource.ping()
|
||||
} else {
|
||||
console.debug("Fetched latest tags for ", id, "but detected no changes")
|
||||
}
|
||||
return currentTags
|
||||
} catch (e) {
|
||||
console.error("Updating the tags of selected element ", id, "failed due to", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ export class ImageUploadManager {
|
|||
description,
|
||||
file,
|
||||
targetKey,
|
||||
tags.data["_orig_theme"]
|
||||
tags?.data?.["_orig_theme"]
|
||||
)
|
||||
if (!isNaN(Number(featureId))) {
|
||||
// This is a map note
|
||||
|
|
|
@ -97,7 +97,10 @@ export abstract class Store<T> implements Readable<T> {
|
|||
abstract map<J>(f: (t: T) => J): Store<J>
|
||||
abstract map<J>(f: (t: T) => J, extraStoresToWatch: Store<any>[]): Store<J>
|
||||
|
||||
public mapD<J>(f: (t: Exclude<T, undefined | null>) => J, extraStoresToWatch?: Store<any>[]): Store<J> {
|
||||
public mapD<J>(
|
||||
f: (t: Exclude<T, undefined | null>) => J,
|
||||
extraStoresToWatch?: Store<any>[]
|
||||
): Store<J> {
|
||||
return this.map((t) => {
|
||||
if (t === undefined) {
|
||||
return undefined
|
||||
|
@ -105,7 +108,7 @@ export abstract class Store<T> implements Readable<T> {
|
|||
if (t === null) {
|
||||
return null
|
||||
}
|
||||
return f(<Exclude<T, undefined | null>> t)
|
||||
return f(<Exclude<T, undefined | null>>t)
|
||||
}, extraStoresToWatch)
|
||||
}
|
||||
|
||||
|
@ -201,24 +204,36 @@ export abstract class Store<T> implements Readable<T> {
|
|||
mapped.addCallbackAndRun((newEventSource) => {
|
||||
if (newEventSource === null) {
|
||||
sink.setData(null)
|
||||
} else if (newEventSource === undefined) {
|
||||
return
|
||||
}
|
||||
if (newEventSource === undefined) {
|
||||
sink.setData(undefined)
|
||||
} else if (!seenEventSources.has(newEventSource)) {
|
||||
return
|
||||
}
|
||||
if (seenEventSources.has(newEventSource)) {
|
||||
// Already seen, so we don't have to add a callback, just update the value
|
||||
sink.setData(newEventSource.data)
|
||||
return
|
||||
}
|
||||
seenEventSources.add(newEventSource)
|
||||
newEventSource.addCallbackAndRun((resultData) => {
|
||||
if (mapped.data === newEventSource) {
|
||||
sink.setData(resultData)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Already seen, so we don't have to add a callback, just update the value
|
||||
sink.setData(newEventSource.data)
|
||||
}
|
||||
})
|
||||
|
||||
return sink
|
||||
}
|
||||
|
||||
public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>): Store<X> {
|
||||
return this.bind((t) => {
|
||||
if (t === undefined || t === null) {
|
||||
return <undefined | null>t
|
||||
}
|
||||
return f(<Exclude<T, undefined | null>>t)
|
||||
})
|
||||
}
|
||||
public stabilized(millisToStabilize): Store<T> {
|
||||
if (Utils.runningFromConsole) {
|
||||
return this
|
||||
|
@ -771,7 +786,10 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
* Monoidal map which results in a read-only store. 'undefined' is passed 'as is'
|
||||
* Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)'
|
||||
*/
|
||||
public mapD<J>(f: (t: Exclude<T, undefined | null>) => J, extraSources: Store<any>[] = []): Store<J | undefined> {
|
||||
public mapD<J>(
|
||||
f: (t: Exclude<T, undefined | null>) => J,
|
||||
extraSources: Store<any>[] = []
|
||||
): Store<J | undefined> {
|
||||
return new MappedStore(
|
||||
this,
|
||||
(t) => {
|
||||
|
@ -781,11 +799,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
if (t === null) {
|
||||
return null
|
||||
}
|
||||
return f(<Exclude<T, undefined | null>> t)
|
||||
return f(<Exclude<T, undefined | null>>t)
|
||||
},
|
||||
extraSources,
|
||||
this._callbacks,
|
||||
(this.data === undefined || this.data === null) ?(<undefined | null> this.data) : f(<any> this.data)
|
||||
this.data === undefined || this.data === null
|
||||
? <undefined | null>this.data
|
||||
: f(<any>this.data)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,11 @@ export class MenuState {
|
|||
this.highlightedUserSetting.setData(undefined)
|
||||
}
|
||||
})
|
||||
this.menuViewTab.addCallbackD((tab) => {
|
||||
if (tab !== "settings") {
|
||||
this.highlightedUserSetting.setData(undefined)
|
||||
}
|
||||
})
|
||||
this.themeViewTab.addCallbackAndRun((tab) => {
|
||||
if (tab !== "filters") {
|
||||
this.highlightedLayerInFilters.setData(undefined)
|
||||
|
|
|
@ -475,9 +475,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
{ nomod: "Escape", onUp: true },
|
||||
Translations.t.hotkeyDocumentation.closeSidebar,
|
||||
() => {
|
||||
if (this.previewedImage.data !== undefined) {
|
||||
this.previewedImage.setData(undefined)
|
||||
return
|
||||
}
|
||||
this.selectedElement.setData(undefined)
|
||||
this.guistate.closeAll()
|
||||
this.previewedImage.setData(undefined)
|
||||
this.focusOnMap()
|
||||
}
|
||||
)
|
||||
|
|
|
@ -36,7 +36,9 @@
|
|||
dispatcher("submit", e.dataTransfer.files)
|
||||
}}
|
||||
>
|
||||
<label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")} for={"fileinput" + id}>
|
||||
<label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")}
|
||||
tabindex="0" for={"fileinput" + id}
|
||||
on:click={() => {console.log("Clicked", inputElement); inputElement.click()}}>
|
||||
<slot />
|
||||
</label>
|
||||
<input
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
|
@ -9,6 +9,12 @@
|
|||
const dispatch = createEventDispatcher<{ close }>()
|
||||
|
||||
export let extraClasses = "p-4 md:p-6"
|
||||
|
||||
let mainContent: HTMLElement
|
||||
onMount(() => {
|
||||
console.log("Mounting floatover")
|
||||
mainContent?.focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -18,7 +24,7 @@
|
|||
dispatch("close")
|
||||
}}
|
||||
>
|
||||
<div class="content normal-background" on:click|stopPropagation={() => {}}>
|
||||
<div bind:this={mainContent} class="content normal-background" on:click|stopPropagation={() => {}}>
|
||||
<div class="h-full rounded-xl">
|
||||
<slot />
|
||||
</div>
|
||||
|
|
|
@ -1,25 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { Utils } from "../../Utils";
|
||||
|
||||
/**
|
||||
* The slotted element will be shown on the right side
|
||||
*/
|
||||
const dispatch = createEventDispatcher<{ close }>()
|
||||
const dispatch = createEventDispatcher<{ close }>();
|
||||
let mainContent: HTMLElement;
|
||||
|
||||
|
||||
onMount(() => {
|
||||
window.setTimeout(
|
||||
() => Utils.focusOnFocusableChild(mainContent), 250
|
||||
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={mainContent}
|
||||
class="absolute top-0 right-0 h-screen w-full overflow-y-auto drop-shadow-2xl md:w-6/12 lg:w-5/12 xl:w-4/12"
|
||||
style="max-width: 100vw; max-height: 100vh"
|
||||
>
|
||||
<div class="normal-background m-0 flex flex-col">
|
||||
<slot name="close-button">
|
||||
<div
|
||||
<button
|
||||
class="absolute right-10 top-10 h-8 w-8 cursor-pointer"
|
||||
on:click={() => dispatch("close")}
|
||||
>
|
||||
<XCircleIcon />
|
||||
</div>
|
||||
</button>
|
||||
</slot>
|
||||
<slot />
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="tabbedgroup flex h-full w-full">
|
||||
<div class="tabbedgroup flex h-full w-full focusable">
|
||||
<TabGroup
|
||||
class="flex h-full w-full flex-col"
|
||||
defaultIndex={1}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
href={LinkToWeblate.hrefToWeblate($language, context)}
|
||||
target="_blank"
|
||||
class="weblate-link mx-1"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Translate class="font-gray" />
|
||||
</a>
|
||||
|
@ -27,6 +28,7 @@
|
|||
href={LinkToWeblate.hrefToWeblate($language, context)}
|
||||
class="weblate-link hidden-on-mobile mx-1"
|
||||
target="_blank"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Translate class="font-gray inline-block" />
|
||||
</a>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Svg from "../../Svg.js"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
@ -15,7 +14,6 @@
|
|||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
|
||||
export let bounds: UIEventSource<BBox>
|
||||
export let selectedElement: UIEventSource<Feature> | undefined = undefined
|
||||
export let selectedLayer: UIEventSource<LayerConfig> | undefined = undefined
|
||||
|
||||
export let clearAfterView: boolean = true
|
||||
|
||||
|
@ -34,9 +32,12 @@
|
|||
let feedback: string = undefined
|
||||
|
||||
Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => {
|
||||
feedback = undefined
|
||||
requestAnimationFrame(() => {
|
||||
inputElement?.focus()
|
||||
inputElement?.select()
|
||||
})
|
||||
})
|
||||
|
||||
const dispatch = createEventDispatcher<{ searchCompleted; searchIsValid: boolean }>()
|
||||
$: {
|
||||
|
@ -73,8 +74,12 @@
|
|||
const layers = Array.from(perLayer?.values() ?? [])
|
||||
for (const layer of layers) {
|
||||
const found = layer.features.data.find((f) => f.properties.id === id)
|
||||
selectedElement?.setData(found)
|
||||
selectedLayer?.setData(layer.layer.layerDef)
|
||||
if (found === undefined) {
|
||||
continue;
|
||||
}
|
||||
selectedElement?.setData(found);
|
||||
console.log("Found an element that probably matches:", selectedElement?.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (clearAfterView) {
|
||||
|
|
|
@ -1,35 +1,25 @@
|
|||
<script lang="ts">
|
||||
import type { Feature } from "geojson"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import type { Feature } from "geojson";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let layer: LayerConfig
|
||||
export let selectedElement: Feature
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let state: SpecialVisualizationState;
|
||||
export let layer: LayerConfig;
|
||||
export let selectedElement: Feature;
|
||||
let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(selectedElement.properties.id);
|
||||
$: {
|
||||
tags = state.featureProperties.getStore(selectedElement.properties.id);
|
||||
}
|
||||
|
||||
let _tags: Record<string, string>
|
||||
onDestroy(
|
||||
tags.addCallbackAndRun((tags) => {
|
||||
_tags = tags
|
||||
})
|
||||
)
|
||||
|
||||
let _metatags: Record<string, string>
|
||||
onDestroy(
|
||||
state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
|
||||
_metatags = tags
|
||||
})
|
||||
)
|
||||
let metatags: Store<Record<string, string>> = state.userRelatedState.preferencesAsTags;
|
||||
</script>
|
||||
|
||||
{#if _tags._deleted === "yes"}
|
||||
{#if $tags._deleted === "yes"}
|
||||
<Tr t={Translations.t.delete.isDeleted} />
|
||||
{:else}
|
||||
<div
|
||||
|
@ -44,7 +34,7 @@
|
|||
class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 p-1 pt-0.5 sm:pt-1"
|
||||
>
|
||||
{#each layer.titleIcons as titleIconConfig}
|
||||
{#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ..._metatags, ..._tags } ) ?? true) && titleIconConfig.IsKnown(_tags)}
|
||||
{#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...$metatags, ...$tags }) ?? true) && titleIconConfig.IsKnown($tags)}
|
||||
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
|
||||
<TagRenderingAnswer
|
||||
config={titleIconConfig}
|
||||
|
@ -59,10 +49,10 @@
|
|||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<XCircleIcon
|
||||
class="h-8 w-8 cursor-pointer"
|
||||
on:click={() => state.selectedElement.setData(undefined)}
|
||||
/>
|
||||
|
||||
<button on:click={() => state.selectedElement.setData(undefined)} class="border-none p-0">
|
||||
<XCircleIcon class="h-8 w-8" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type { Feature } from "geojson"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let layer: LayerConfig
|
||||
export let selectedElement: Feature
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let highlightedRendering: UIEventSource<string> = undefined
|
||||
|
||||
let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(selectedElement.properties.id)
|
||||
$: {
|
||||
tags = state.featureProperties.getStore(selectedElement.properties.id)
|
||||
}
|
||||
let _metatags: Record<string, string>
|
||||
onDestroy(
|
||||
state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
|
||||
|
@ -21,20 +25,12 @@
|
|||
})
|
||||
)
|
||||
|
||||
let knownTagRenderings = layer.tagRenderings.filter(
|
||||
let knownTagRenderings: Store<TagRenderingConfig[]> = tags.mapD(tgs => layer.tagRenderings.filter(
|
||||
(config) =>
|
||||
(config.condition?.matchesProperties($tags) ?? true) &&
|
||||
config.metacondition?.matchesProperties({ ...$tags, ..._metatags } ?? true) &&
|
||||
config.IsKnown($tags)
|
||||
)
|
||||
$: {
|
||||
knownTagRenderings = layer.tagRenderings.filter(
|
||||
(config) =>
|
||||
(config.condition?.matchesProperties($tags) ?? true) &&
|
||||
config.metacondition?.matchesProperties({ ...$tags, ..._metatags } ?? true) &&
|
||||
config.IsKnown($tags)
|
||||
)
|
||||
}
|
||||
(config.condition?.matchesProperties(tgs) ?? true) &&
|
||||
config.metacondition?.matchesProperties({ ...tgs, ..._metatags } ?? true) &&
|
||||
config.IsKnown(tgs)
|
||||
))
|
||||
</script>
|
||||
|
||||
{#if $tags._deleted === "yes"}
|
||||
|
@ -43,8 +39,8 @@
|
|||
<Tr t={Translations.t.general.returnToTheMap} />
|
||||
</button>
|
||||
{:else}
|
||||
<div class="flex h-full flex-col gap-y-2 overflow-y-auto p-1 px-2">
|
||||
{#each knownTagRenderings as config (config.id)}
|
||||
<div class="flex h-full flex-col gap-y-2 overflow-y-auto p-1 px-2 focusable" tabindex="-1">
|
||||
{#each $knownTagRenderings as config (config.id)}
|
||||
<TagRenderingEditable
|
||||
{tags}
|
||||
{config}
|
||||
|
@ -52,7 +48,7 @@
|
|||
{selectedElement}
|
||||
{layer}
|
||||
{highlightedRendering}
|
||||
clss={knownTagRenderings.length === 1 ? "h-full" : "tr-length-" + knownTagRenderings.length}
|
||||
clss={$knownTagRenderings.length === 1 ? "h-full" : "tr-length-" + $knownTagRenderings.length}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
let selectedElement = state.selectedElement
|
||||
let selectedLayer = state.selectedLayer
|
||||
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
let searchEnabled = false
|
||||
|
@ -116,7 +115,6 @@
|
|||
}}
|
||||
perLayer={state.perLayer}
|
||||
{selectedElement}
|
||||
{selectedLayer}
|
||||
{triggerSearch}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
}
|
||||
|
||||
function select() {
|
||||
state.selectedLayer.setData(favConfig);
|
||||
state.selectedElement.setData(feature);
|
||||
center();
|
||||
}
|
||||
|
|
|
@ -13,11 +13,12 @@
|
|||
}
|
||||
|
||||
let imgEl: HTMLImageElement
|
||||
export let imgClass: string = undefined
|
||||
</script>
|
||||
|
||||
|
||||
<div class="relative">
|
||||
<img bind:this={imgEl} src={image.url} on:error={(event) => {
|
||||
<img bind:this={imgEl} src={image.url} class={imgClass ?? ""} on:error={(event) => {
|
||||
if(fallbackImage){
|
||||
imgEl.src = fallbackImage
|
||||
}
|
||||
|
|
|
@ -7,9 +7,10 @@ import ImageAttribution from "./ImageAttribution.svelte"
|
|||
import ImagePreview from "./ImagePreview.svelte"
|
||||
import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { Utils } from "../../Utils"
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export let image: ProvidedImage
|
||||
|
||||
export let clss: string = undefined
|
||||
async function download() {
|
||||
const response = await fetch(image.url)
|
||||
const blob = await response.blob()
|
||||
|
@ -20,7 +21,7 @@ async function download() {
|
|||
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full relative">
|
||||
<div class={twMerge("w-full h-full relative", clss)}>
|
||||
<div class="absolute top-0 left-0 w-full h-full overflow-hidden">
|
||||
<ImagePreview image={image} />
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
$: {
|
||||
if (panzoomEl) {
|
||||
panzoomInstance = panzoom(panzoomEl, { bounds: true,
|
||||
boundsPadding: 1,
|
||||
boundsPadding: 0.49,
|
||||
minZoom: 1,
|
||||
maxZoom: 25,
|
||||
initialZoom: 1.2
|
||||
|
|
|
@ -64,7 +64,9 @@
|
|||
</script>
|
||||
|
||||
<div class="flex w-fit shrink-0 flex-col">
|
||||
<AttributedImage image={providedImage} />
|
||||
<div on:click={() => state.previewedImage.setData(providedImage)}>
|
||||
<AttributedImage image={providedImage} imgClass="max-h-64 w-auto"/>
|
||||
</div>
|
||||
{#if linkable}
|
||||
<label>
|
||||
<input bind:checked={isLinked} type="checkbox" />
|
||||
|
|
|
@ -1,44 +1,41 @@
|
|||
<script lang="ts">
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import NearbyImages from "./NearbyImages.svelte"
|
||||
import Svg from "../../Svg"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import exp from "constants"
|
||||
import Camera_plus from "../../assets/svg/Camera_plus.svelte"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import type { OsmTags } from "../../Models/OsmFeature";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import type { Feature } from "geojson";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import NearbyImages from "./NearbyImages.svelte";
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid";
|
||||
import Camera_plus from "../../assets/svg/Camera_plus.svelte";
|
||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||
|
||||
export let tags: Store<OsmTags>
|
||||
export let state: SpecialVisualizationState
|
||||
export let lon: number
|
||||
export let lat: number
|
||||
export let feature: Feature
|
||||
export let tags: Store<OsmTags>;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let lon: number;
|
||||
export let lat: number;
|
||||
export let feature: Feature;
|
||||
|
||||
export let linkable: boolean = true
|
||||
export let layer: LayerConfig
|
||||
const t = Translations.t.image.nearby
|
||||
export let linkable: boolean = true;
|
||||
export let layer: LayerConfig;
|
||||
const t = Translations.t.image.nearby;
|
||||
|
||||
let expanded = false
|
||||
let expanded = false;
|
||||
</script>
|
||||
<LoginToggle {state}>
|
||||
|
||||
{#if expanded}
|
||||
{#if expanded}
|
||||
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable}>
|
||||
<XCircleIcon
|
||||
slot="corner"
|
||||
class="h-6 w-6 cursor-pointer"
|
||||
<button slot="corner"
|
||||
class="h-6 w-6 cursor-pointer no-image-background p-0 border-none"
|
||||
on:click={() => {
|
||||
expanded = false
|
||||
}}
|
||||
/>
|
||||
}}>
|
||||
<XCircleIcon />
|
||||
</button>
|
||||
</NearbyImages>
|
||||
{:else}
|
||||
{:else}
|
||||
<button
|
||||
class="flex w-full items-center"
|
||||
on:click={() => {
|
||||
|
@ -48,5 +45,5 @@
|
|||
<Camera_plus class="mr-2 block h-8 w-8 p-1" />
|
||||
<Tr t={t.seeNearby} />
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</LoginToggle>
|
||||
|
|
|
@ -56,9 +56,9 @@
|
|||
multiple={true}
|
||||
on:submit={(e) => handleFiles(e.detail)}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center" >
|
||||
{#if image !== undefined}
|
||||
<img src={image} />
|
||||
<img src={image} aria-hidden="true" />
|
||||
{:else}
|
||||
<Camera_plus class="block h-12 w-12 p-1 text-4xl" />
|
||||
{/if}
|
||||
|
@ -70,15 +70,15 @@
|
|||
</div>
|
||||
</FileSelector>
|
||||
<div class="text-sm">
|
||||
<Tr t={t.respectPrivacy} />
|
||||
<a
|
||||
class="cursor-pointer"
|
||||
<button
|
||||
class="link small "
|
||||
on:click={() => {
|
||||
state.guistate.openUsersettings("picture-license")
|
||||
}}
|
||||
>
|
||||
<Tr t={t.currentLicense.Subs({ license: $licenseStore })} />
|
||||
</a>
|
||||
</button>
|
||||
<Tr t={t.respectPrivacy} />
|
||||
</div>
|
||||
</div>
|
||||
</LoginToggle>
|
||||
|
|
|
@ -302,19 +302,23 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
rescaleIcons: number,
|
||||
pixelRatio: number
|
||||
) {
|
||||
const marker = element
|
||||
const style = marker.style.transform
|
||||
let x = marker.getBoundingClientRect().x
|
||||
let y = marker.getBoundingClientRect().y
|
||||
marker.style.transform = ""
|
||||
const style = element.style.transform
|
||||
let x = element.getBoundingClientRect().x
|
||||
let y = element.getBoundingClientRect().y
|
||||
element.style.transform = ""
|
||||
const offset = style.match(/translate\(([-0-9]+)%, ?([-0-9]+)%\)/)
|
||||
|
||||
const w = marker.style.width
|
||||
const w = element.style.width
|
||||
const h = element.style.height
|
||||
|
||||
// Force a wider view for icon badges
|
||||
marker.style.width = marker.getBoundingClientRect().width * 4 + "px"
|
||||
const svgSource = await htmltoimage.toSvg(marker)
|
||||
element.style.width = element.getBoundingClientRect().width * 4 + "px"
|
||||
element.style.height = element.getBoundingClientRect().height + "px"
|
||||
const svgSource = await htmltoimage.toSvg(element)
|
||||
const img = await MapLibreAdaptor.createImage(svgSource)
|
||||
marker.style.width = w
|
||||
element.style.width = w
|
||||
element.style.height = h
|
||||
|
||||
if (offset && rescaleIcons !== 1) {
|
||||
const [_, __, relYStr] = offset
|
||||
const relY = Number(relYStr)
|
||||
|
|
|
@ -410,9 +410,14 @@ class LineRenderingLayer {
|
|||
this._listenerInstalledOn.add(id)
|
||||
tags.addCallbackAndRunD((properties) => {
|
||||
// Make sure to use 'getSource' here, the layer names are different!
|
||||
try {
|
||||
if (map.getSource(this._layername) === undefined) {
|
||||
return true
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug("Could not fetch source for", this._layername)
|
||||
return
|
||||
}
|
||||
map.setFeatureState(
|
||||
{ source: this._layername, id },
|
||||
this.calculatePropsFor(properties)
|
||||
|
|
|
@ -5,14 +5,13 @@
|
|||
import TagRenderingMapping from "./TagRenderingMapping.svelte"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import type { Feature } from "geojson"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export let tags: UIEventSource<Record<string, string> | undefined>
|
||||
let _tags: Record<string, string>
|
||||
let trs: { then: Translation; icon?: string; iconClass?: string }[]
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let selectedElement: Feature
|
||||
|
@ -23,22 +22,18 @@
|
|||
if (config === undefined) {
|
||||
throw "Config is undefined in tagRenderingAnswer"
|
||||
}
|
||||
onDestroy(
|
||||
tags.addCallbackAndRun((tags) => {
|
||||
_tags = tags
|
||||
trs = Utils.NoNull(config?.GetRenderValues(_tags))
|
||||
})
|
||||
)
|
||||
let trs : Store<{then: Translation, icon?: string, iconClass?: string}[]> = tags.mapD(tags => Utils.NoNull(config?.GetRenderValues(tags)))
|
||||
|
||||
</script>
|
||||
|
||||
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(_tags))}
|
||||
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties($tags))}
|
||||
<div class={twMerge("link-underline inline-block w-full", config?.classes, extraClasses)}>
|
||||
{#if trs.length === 1}
|
||||
<TagRenderingMapping mapping={trs[0]} {tags} {state} {selectedElement} {layer} />
|
||||
{#if $trs.length === 1}
|
||||
<TagRenderingMapping mapping={$trs[0]} {tags} {state} {selectedElement} {layer} />
|
||||
{/if}
|
||||
{#if trs.length > 1}
|
||||
{#if $trs.length > 1}
|
||||
<ul>
|
||||
{#each trs as mapping}
|
||||
{#each $trs as mapping}
|
||||
<li>
|
||||
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer} />
|
||||
</li>
|
||||
|
|
|
@ -1,40 +1,41 @@
|
|||
<script lang="ts">
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import TagRenderingAnswer from "./TagRenderingAnswer.svelte"
|
||||
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import Translations from "../../i18n/Translations.js"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
export let config: TagRenderingConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let selectedElement: Feature | undefined
|
||||
export let state: SpecialVisualizationState
|
||||
export let layer: LayerConfig = undefined
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { Feature } from "geojson";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import TagRenderingAnswer from "./TagRenderingAnswer.svelte";
|
||||
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import { Utils } from "../../../Utils";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge
|
||||
export let config: TagRenderingConfig;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let selectedElement: Feature | undefined;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let layer: LayerConfig = undefined;
|
||||
|
||||
export let highlightedRendering: UIEventSource<string> = undefined
|
||||
export let clss
|
||||
export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge;
|
||||
|
||||
export let highlightedRendering: UIEventSource<string> = undefined;
|
||||
export let clss;
|
||||
/**
|
||||
* Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property
|
||||
*/
|
||||
export let editMode = !config.IsKnown(tags.data) // || showQuestionIfUnknown;
|
||||
export let editMode = !config.IsKnown(tags.data); // || showQuestionIfUnknown;
|
||||
if (tags) {
|
||||
onDestroy(
|
||||
tags.addCallbackD((tags) => {
|
||||
editMode = !config.IsKnown(tags)
|
||||
editMode = !config.IsKnown(tags);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let htmlElem: HTMLDivElement
|
||||
let htmlElem: HTMLDivElement;
|
||||
$: {
|
||||
if (editMode && htmlElem !== undefined && config.IsKnown(tags)) {
|
||||
// EditMode switched to true yet the answer is already known, so the person wants to make a change
|
||||
|
@ -42,32 +43,36 @@
|
|||
|
||||
// Some delay is applied to give Svelte the time to render the _question_
|
||||
window.setTimeout(() => {
|
||||
Utils.scrollIntoView(<any>htmlElem)
|
||||
}, 50)
|
||||
Utils.scrollIntoView(<any>htmlElem);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
const _htmlElement = new UIEventSource<HTMLElement>(undefined)
|
||||
$: _htmlElement.setData(htmlElem)
|
||||
const _htmlElement = new UIEventSource<HTMLElement>(undefined);
|
||||
$: _htmlElement.setData(htmlElem);
|
||||
|
||||
function setHighlighting() {
|
||||
if (highlightedRendering === undefined) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (htmlElem === undefined) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const highlighted = highlightedRendering.data
|
||||
const highlighted = highlightedRendering.data;
|
||||
if (config.id === highlighted) {
|
||||
htmlElem.classList.add("glowing-shadow")
|
||||
htmlElem.classList.add("glowing-shadow");
|
||||
htmlElem.tabIndex = "-1";
|
||||
console.log("Scrolling to", htmlElem);
|
||||
htmlElem.scrollIntoView({ behavior: "smooth" });
|
||||
Utils.focusOnFocusableChild(htmlElem);
|
||||
} else {
|
||||
htmlElem.classList.remove("glowing-shadow")
|
||||
htmlElem.classList.remove("glowing-shadow");
|
||||
}
|
||||
}
|
||||
|
||||
if (highlightedRendering) {
|
||||
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()))
|
||||
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()))
|
||||
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()));
|
||||
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -84,13 +89,13 @@
|
|||
>
|
||||
<Tr t={Translations.t.general.cancel} />
|
||||
</button>
|
||||
<XCircleIcon
|
||||
slot="upper-right"
|
||||
class="h-8 w-8 cursor-pointer"
|
||||
<button slot="upper-right"
|
||||
class="h-8 w-8 cursor-pointer border-none p-0"
|
||||
on:click={() => {
|
||||
editMode = false
|
||||
}}
|
||||
/>
|
||||
}}>
|
||||
<XCircleIcon />
|
||||
</button>
|
||||
</TagRenderingQuestion>
|
||||
{:else}
|
||||
<div class="low-interaction flex items-center justify-between overflow-hidden rounded px-2">
|
||||
|
|
|
@ -136,7 +136,6 @@
|
|||
|
||||
function onSave() {
|
||||
if (selectedTags === undefined) {
|
||||
console.log("SelectedTags is undefined, ignoring 'onSave'-event");
|
||||
return;
|
||||
}
|
||||
if (layer === undefined || (layer?.source === null && layer.id !== "favourite")) {
|
||||
|
|
|
@ -1,138 +1,116 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import MapControlButton from "./Base/MapControlButton.svelte"
|
||||
import ToSvelte from "./Base/ToSvelte.svelte"
|
||||
import If from "./Base/If.svelte"
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl"
|
||||
import type { Feature } from "geojson"
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import Filterview from "./BigComponents/Filterview.svelte"
|
||||
import ThemeViewState from "../Models/ThemeViewState"
|
||||
import type { MapProperties } from "../Models/MapProperties"
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte"
|
||||
import Translations from "./i18n/Translations"
|
||||
import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Tr from "./Base/Tr.svelte"
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
|
||||
import FloatOver from "./Base/FloatOver.svelte"
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
|
||||
import Constants from "../Models/Constants"
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte"
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||
import LoginToggle from "./Base/LoginToggle.svelte"
|
||||
import LoginButton from "./Base/LoginButton.svelte"
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
||||
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
|
||||
import ModalRight from "./Base/ModalRight.svelte"
|
||||
import { Utils } from "../Utils"
|
||||
import Hotkeys from "./Base/Hotkeys"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
|
||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
|
||||
import type { RasterLayerPolygon } from "../Models/RasterLayers"
|
||||
import { AvailableRasterLayers } from "../Models/RasterLayers"
|
||||
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
|
||||
import IfHidden from "./Base/IfHidden.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
|
||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
|
||||
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import StateIndicator from "./BigComponents/StateIndicator.svelte"
|
||||
import ShareScreen from "./BigComponents/ShareScreen.svelte"
|
||||
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
|
||||
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
|
||||
import Cross from "../assets/svg/Cross.svelte"
|
||||
import Summary from "./BigComponents/Summary.svelte"
|
||||
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
|
||||
import Mastodon from "../assets/svg/Mastodon.svelte"
|
||||
import Bug from "../assets/svg/Bug.svelte"
|
||||
import Liberapay from "../assets/svg/Liberapay.svelte"
|
||||
import OpenJosm from "./Base/OpenJosm.svelte"
|
||||
import Min from "../assets/svg/Min.svelte"
|
||||
import Plus from "../assets/svg/Plus.svelte"
|
||||
import Filter from "../assets/svg/Filter.svelte"
|
||||
import Add from "../assets/svg/Add.svelte"
|
||||
import Statistics from "../assets/svg/Statistics.svelte"
|
||||
import Community from "../assets/svg/Community.svelte"
|
||||
import Download from "../assets/svg/Download.svelte"
|
||||
import Share from "../assets/svg/Share.svelte"
|
||||
import Favourites from "./Favourites/Favourites.svelte"
|
||||
import ImageOperations from "./Image/ImageOperations.svelte"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte";
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||
import MapControlButton from "./Base/MapControlButton.svelte";
|
||||
import ToSvelte from "./Base/ToSvelte.svelte";
|
||||
import If from "./Base/If.svelte";
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl";
|
||||
import type { Feature } from "geojson";
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import Filterview from "./BigComponents/Filterview.svelte";
|
||||
import ThemeViewState from "../Models/ThemeViewState";
|
||||
import type { MapProperties } from "../Models/MapProperties";
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte";
|
||||
import Translations from "./i18n/Translations";
|
||||
import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import Tr from "./Base/Tr.svelte";
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
|
||||
import FloatOver from "./Base/FloatOver.svelte";
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
|
||||
import Constants from "../Models/Constants";
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte";
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState";
|
||||
import LoginToggle from "./Base/LoginToggle.svelte";
|
||||
import LoginButton from "./Base/LoginButton.svelte";
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel";
|
||||
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte";
|
||||
import ModalRight from "./Base/ModalRight.svelte";
|
||||
import { Utils } from "../Utils";
|
||||
import Hotkeys from "./Base/Hotkeys";
|
||||
import SvelteUIElement from "./Base/SvelteUIElement";
|
||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte";
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
|
||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
|
||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
|
||||
import type { RasterLayerPolygon } from "../Models/RasterLayers";
|
||||
import { AvailableRasterLayers } from "../Models/RasterLayers";
|
||||
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
|
||||
import IfHidden from "./Base/IfHidden.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
import MapillaryLink from "./BigComponents/MapillaryLink.svelte";
|
||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte";
|
||||
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
|
||||
import StateIndicator from "./BigComponents/StateIndicator.svelte";
|
||||
import ShareScreen from "./BigComponents/ShareScreen.svelte";
|
||||
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte";
|
||||
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte";
|
||||
import Cross from "../assets/svg/Cross.svelte";
|
||||
import Summary from "./BigComponents/Summary.svelte";
|
||||
import LanguagePicker from "./InputElement/LanguagePicker.svelte";
|
||||
import Mastodon from "../assets/svg/Mastodon.svelte";
|
||||
import Bug from "../assets/svg/Bug.svelte";
|
||||
import Liberapay from "../assets/svg/Liberapay.svelte";
|
||||
import OpenJosm from "./Base/OpenJosm.svelte";
|
||||
import Min from "../assets/svg/Min.svelte";
|
||||
import Plus from "../assets/svg/Plus.svelte";
|
||||
import Filter from "../assets/svg/Filter.svelte";
|
||||
import Add from "../assets/svg/Add.svelte";
|
||||
import Statistics from "../assets/svg/Statistics.svelte";
|
||||
import Community from "../assets/svg/Community.svelte";
|
||||
import Download from "../assets/svg/Download.svelte";
|
||||
import Share from "../assets/svg/Share.svelte";
|
||||
import Favourites from "./Favourites/Favourites.svelte";
|
||||
import ImageOperations from "./Image/ImageOperations.svelte";
|
||||
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
export let state: ThemeViewState;
|
||||
let layout = state.layout;
|
||||
|
||||
let maplibremap: UIEventSource<MlMap> = state.map
|
||||
let selectedElement: UIEventSource<Feature> = state.selectedElement
|
||||
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
|
||||
let maplibremap: UIEventSource<MlMap> = state.map;
|
||||
let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined);
|
||||
|
||||
let currentZoom = state.mapProperties.zoom
|
||||
let showCrosshair = state.userRelatedState.showCrosshair
|
||||
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation
|
||||
let centerFeatures = state.closestFeatures.features
|
||||
const selectedElementView = selectedElement.map(
|
||||
(selectedElement) => {
|
||||
// Svelte doesn't properly reload some of the legacy UI-elements
|
||||
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
||||
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
||||
const layer = selectedLayer.data
|
||||
if (selectedElement === undefined || layer === undefined) {
|
||||
return undefined
|
||||
state.selectedElement.addCallback(selected => {
|
||||
if(!selected){
|
||||
selectedElement.setData(selected)
|
||||
return
|
||||
}
|
||||
|
||||
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
|
||||
return undefined
|
||||
if(selected !== selectedElement.data){
|
||||
// We first set the selected element to 'undefined' to force the popup to close...
|
||||
selectedElement.setData(undefined)
|
||||
}
|
||||
// ... we give svelte some time to update with requestAnimationFrame ...
|
||||
window.requestAnimationFrame(() => {
|
||||
// ... and we force a fresh popup window
|
||||
selectedElement.setData(selected)
|
||||
})
|
||||
|
||||
const tags = state.featureProperties.getStore(selectedElement.properties.id)
|
||||
return new SvelteUIElement(SelectedElementView, {
|
||||
state,
|
||||
layer,
|
||||
selectedElement,
|
||||
tags,
|
||||
}).SetClass("h-full w-full")
|
||||
},
|
||||
[selectedLayer],
|
||||
)
|
||||
})
|
||||
|
||||
const selectedElementTitle = selectedElement.map(
|
||||
(selectedElement) => {
|
||||
// Svelte doesn't properly reload some of the legacy UI-elements
|
||||
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
||||
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
||||
const layer = selectedLayer.data
|
||||
if (selectedElement === undefined || layer === undefined) {
|
||||
return undefined
|
||||
}
|
||||
let selectedLayer: UIEventSource<LayerConfig> = state.selectedElement.mapD(element => state.layout.getMatchingLayer(element.properties));
|
||||
|
||||
const tags = state.featureProperties.getStore(selectedElement.properties.id)
|
||||
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags })
|
||||
},
|
||||
[selectedLayer],
|
||||
)
|
||||
let currentZoom = state.mapProperties.zoom;
|
||||
let showCrosshair = state.userRelatedState.showCrosshair;
|
||||
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation;
|
||||
let centerFeatures = state.closestFeatures.features;
|
||||
|
||||
let mapproperties: MapProperties = state.mapProperties
|
||||
let featureSwitches: FeatureSwitchState = state.featureSwitches
|
||||
let availableLayers = state.availableLayers
|
||||
let userdetails = state.osmConnection.userDetails
|
||||
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
|
||||
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
|
||||
|
||||
let mapproperties: MapProperties = state.mapProperties;
|
||||
let featureSwitches: FeatureSwitchState = state.featureSwitches;
|
||||
let availableLayers = state.availableLayers;
|
||||
let userdetails = state.osmConnection.userDetails;
|
||||
let currentViewLayer = layout.layers.find((l) => l.id === "current_view");
|
||||
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer;
|
||||
let rasterLayerName =
|
||||
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name
|
||||
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name;
|
||||
onDestroy(
|
||||
rasterLayer.addCallbackAndRunD((l) => {
|
||||
rasterLayerName = l.properties.name
|
||||
}),
|
||||
)
|
||||
let previewedImage = state.previewedImage
|
||||
rasterLayerName = l.properties.name;
|
||||
})
|
||||
);
|
||||
let previewedImage = state.previewedImage;
|
||||
</script>
|
||||
|
||||
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
|
||||
|
@ -146,7 +124,7 @@
|
|||
<Geosearch
|
||||
bounds={state.mapProperties.bounds}
|
||||
perLayer={state.perLayer}
|
||||
{selectedElement}
|
||||
selectedElement={state.selectedElement}
|
||||
{selectedLayer}
|
||||
/>
|
||||
</div>
|
||||
|
@ -264,9 +242,7 @@
|
|||
<If condition={featureSwitches.featureSwitchGeolocation}>
|
||||
<MapControlButton>
|
||||
<ToSvelte
|
||||
construct={new GeolocationControl(state.geolocation, mapproperties).SetClass(
|
||||
"block w-8 h-8"
|
||||
)}
|
||||
construct={new GeolocationControl(state.geolocation, mapproperties).SetClass("block w-8 h-8")}
|
||||
/>
|
||||
</MapControlButton>
|
||||
</If>
|
||||
|
@ -285,66 +261,52 @@
|
|||
</LoginToggle>
|
||||
|
||||
<If condition={state.previewedImage.map(i => i!==undefined)}>
|
||||
<FloatOver on:close={() => state.previewedImage.setData(undefined)} extraClasses="">
|
||||
<FloatOver extraClasses="p-1" on:close={() => state.previewedImage.setData(undefined)}>
|
||||
<div
|
||||
slot="close-button"
|
||||
class="absolute right-4 top-4 h-8 w-8 cursor-pointer rounded-full hover:bg-white bg-white/50 transition-colors duration-200"
|
||||
on:click={() => previewedImage.setData(undefined)}
|
||||
slot="close-button"
|
||||
>
|
||||
<XCircleIcon />
|
||||
</div>
|
||||
<ImageOperations image={$previewedImage} />
|
||||
<ImageOperations clss="focusable" image={$previewedImage} />
|
||||
</FloatOver>
|
||||
</If>
|
||||
|
||||
<If
|
||||
condition={selectedElementView.map(
|
||||
(v) =>
|
||||
v !== undefined && selectedLayer.data !== undefined && !selectedLayer.data.popupInFloatover,
|
||||
[selectedLayer]
|
||||
)}
|
||||
>
|
||||
{#if $selectedElement !== undefined && $selectedLayer !== undefined && !($selectedLayer.popupInFloatover)}
|
||||
<!-- right modal with the selected element view -->
|
||||
<ModalRight
|
||||
on:close={() => {
|
||||
selectedElement.setData(undefined)
|
||||
}}
|
||||
>
|
||||
<div slot="close-button"/>
|
||||
<div class="normal-background absolute flex h-full w-full flex-col">
|
||||
<ToSvelte construct={new VariableUiElement(selectedElementTitle)}>
|
||||
<!-- Title -->
|
||||
</ToSvelte>
|
||||
<ToSvelte construct={new VariableUiElement(selectedElementView).SetClass("overflow-auto")}>
|
||||
<!-- Main view -->
|
||||
</ToSvelte>
|
||||
<SelectedElementTitle {state} layer={$selectedLayer} selectedElement={$selectedElement} />
|
||||
<SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} />
|
||||
</div>
|
||||
</ModalRight>
|
||||
</If>
|
||||
{/if}
|
||||
|
||||
<If
|
||||
condition={selectedElementView.map(
|
||||
(v) =>
|
||||
v !== undefined && selectedLayer.data !== undefined && selectedLayer.data.popupInFloatover,
|
||||
[selectedLayer]
|
||||
)}
|
||||
>
|
||||
{#if $selectedElement !== undefined && $selectedLayer !== undefined && $selectedLayer.popupInFloatover}
|
||||
<!-- Floatover with the selected element, if applicable -->
|
||||
<FloatOver
|
||||
on:close={() => {
|
||||
selectedElement.setData(undefined)
|
||||
}}
|
||||
>
|
||||
<ToSvelte
|
||||
construct={new VariableUiElement(selectedElementView).SetClass("h-full w-full flex")}
|
||||
/>
|
||||
<div class="h-full w-full flex focusable">
|
||||
<SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} />
|
||||
</div>
|
||||
</FloatOver>
|
||||
</If>
|
||||
{/if}
|
||||
|
||||
<If condition={state.guistate.themeIsOpened}>
|
||||
<!-- Theme menu -->
|
||||
<FloatOver on:close={() => state.guistate.themeIsOpened.setData(false)}>
|
||||
<span slot="close-button"><!-- Disable the close button --></span>
|
||||
<TabbedGroup
|
||||
|
||||
condition1={state.featureSwitches.featureSwitchFilter}
|
||||
tab={state.guistate.themeViewTabIndex}
|
||||
>
|
||||
|
@ -421,7 +383,7 @@
|
|||
state.guistate.backgroundLayerSelectionIsOpened.setData(false)
|
||||
}}
|
||||
>
|
||||
<div class="h-full p-2">
|
||||
<div class="h-full p-2 focusable">
|
||||
<RasterLayerOverview
|
||||
{availableLayers}
|
||||
map={state.map}
|
||||
|
|
101
src/Utils.ts
101
src/Utils.ts
|
@ -11,7 +11,7 @@ export class Utils {
|
|||
public static readonly assets_path = "./assets/svg/"
|
||||
public static externalDownloadFunction: (
|
||||
url: string,
|
||||
headers?: any,
|
||||
headers?: any
|
||||
) => Promise<{ content: string } | { redirect: string }>
|
||||
public static Special_visualizations_tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`.
|
||||
This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature.
|
||||
|
@ -150,7 +150,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
if (Utils.runningFromConsole) {
|
||||
return
|
||||
}
|
||||
DOMPurify.addHook("afterSanitizeAttributes", function(node) {
|
||||
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
|
||||
// set all elements owning target to target=_blank + add noopener noreferrer
|
||||
const target = node.getAttribute("target")
|
||||
if (target) {
|
||||
|
@ -172,7 +172,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
*/
|
||||
public static ParseVisArgs(
|
||||
specs: { name: string; defaultValue?: string }[],
|
||||
args: string[],
|
||||
args: string[]
|
||||
): Record<string, string> {
|
||||
const parsed: Record<string, string> = {}
|
||||
if (args.length > specs.length) {
|
||||
|
@ -324,7 +324,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
object: any,
|
||||
name: string,
|
||||
init: () => any,
|
||||
whenDone?: () => void,
|
||||
whenDone?: () => void
|
||||
) {
|
||||
Object.defineProperty(object, name, {
|
||||
enumerable: false,
|
||||
|
@ -347,7 +347,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
object: any,
|
||||
name: string,
|
||||
init: () => Promise<any>,
|
||||
whenDone?: () => void,
|
||||
whenDone?: () => void
|
||||
) {
|
||||
Object.defineProperty(object, name, {
|
||||
enumerable: false,
|
||||
|
@ -487,7 +487,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
public static SubstituteKeys(
|
||||
txt: string | undefined,
|
||||
tags: Record<string, any> | undefined,
|
||||
useLang?: string,
|
||||
useLang?: string
|
||||
): string | undefined {
|
||||
if (txt === undefined) {
|
||||
return undefined
|
||||
|
@ -523,7 +523,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
"SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is",
|
||||
key,
|
||||
"\nThe value is",
|
||||
v,
|
||||
v
|
||||
)
|
||||
v = v.InnerConstructElement()?.textContent
|
||||
}
|
||||
|
@ -636,7 +636,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
if (!Array.isArray(targetV)) {
|
||||
throw new Error(
|
||||
"Cannot concatenate: value to add is not an array: " +
|
||||
JSON.stringify(targetV),
|
||||
JSON.stringify(targetV)
|
||||
)
|
||||
}
|
||||
if (Array.isArray(sourceV)) {
|
||||
|
@ -646,7 +646,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
"Could not merge concatenate " +
|
||||
JSON.stringify(sourceV) +
|
||||
" and " +
|
||||
JSON.stringify(targetV),
|
||||
JSON.stringify(targetV)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
@ -691,7 +691,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
path: string[],
|
||||
object: any,
|
||||
replaceLeaf: (leaf: any, travelledPath: string[]) => any,
|
||||
travelledPath: string[] = [],
|
||||
travelledPath: string[] = []
|
||||
): void {
|
||||
if (object == null) {
|
||||
return
|
||||
|
@ -722,7 +722,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
if (Array.isArray(sub)) {
|
||||
sub.forEach((el, i) =>
|
||||
Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, head, "" + i]),
|
||||
Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, head, "" + i])
|
||||
)
|
||||
return
|
||||
}
|
||||
|
@ -739,7 +739,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
path: string[],
|
||||
object: any,
|
||||
collectedList: { leaf: any; path: string[] }[] = [],
|
||||
travelledPath: string[] = [],
|
||||
travelledPath: string[] = []
|
||||
): { leaf: any; path: string[] }[] {
|
||||
if (object === undefined || object === null) {
|
||||
return collectedList
|
||||
|
@ -769,7 +769,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
|
||||
if (Array.isArray(sub)) {
|
||||
sub.forEach((el, i) =>
|
||||
Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i]),
|
||||
Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i])
|
||||
)
|
||||
return collectedList
|
||||
}
|
||||
|
@ -813,7 +813,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
json: any,
|
||||
f: (v: object | number | string | boolean | undefined, path: string[]) => any,
|
||||
isLeaf: (object) => boolean = undefined,
|
||||
path: string[] = [],
|
||||
path: string[] = []
|
||||
) {
|
||||
if (json === undefined || json === null) {
|
||||
return f(json, path)
|
||||
|
@ -852,7 +852,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
json: any,
|
||||
collect: (v: number | string | boolean | undefined, path: string[]) => any,
|
||||
isLeaf: (object) => boolean = undefined,
|
||||
path = [],
|
||||
path = []
|
||||
): void {
|
||||
if (json === undefined) {
|
||||
return
|
||||
|
@ -927,7 +927,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
continue
|
||||
}
|
||||
const i = part.charCodeAt(0)
|
||||
result += "\"" + keys[i] + "\":" + part.substring(1)
|
||||
result += '"' + keys[i] + '":' + part.substring(1)
|
||||
}
|
||||
|
||||
return result
|
||||
|
@ -954,7 +954,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
url: string,
|
||||
headers?: any,
|
||||
method: "POST" | "GET" | "PUT" | "UPDATE" | "DELETE" | "OPTIONS" = "GET",
|
||||
content?: string,
|
||||
content?: string
|
||||
): Promise<
|
||||
| { content: string }
|
||||
| { redirect: string }
|
||||
|
@ -1019,7 +1019,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
public static async downloadJsonCached(
|
||||
url: string,
|
||||
maxCacheTimeMs: number,
|
||||
headers?: any,
|
||||
headers?: any
|
||||
): Promise<any> {
|
||||
const result = await Utils.downloadJsonAdvanced(url, headers)
|
||||
if (result["content"]) {
|
||||
|
@ -1031,7 +1031,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
public static async downloadJsonCachedAdvanced(
|
||||
url: string,
|
||||
maxCacheTimeMs: number,
|
||||
headers?: any,
|
||||
headers?: any
|
||||
): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> {
|
||||
const cached = Utils._download_cache.get(url)
|
||||
if (cached !== undefined) {
|
||||
|
@ -1042,7 +1042,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
const promise =
|
||||
/*NO AWAIT as we work with the promise directly */ Utils.downloadJsonAdvanced(
|
||||
url,
|
||||
headers,
|
||||
headers
|
||||
)
|
||||
Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() })
|
||||
return await promise
|
||||
|
@ -1058,7 +1058,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
|
||||
public static async downloadJsonAdvanced(
|
||||
url: string,
|
||||
headers?: any,
|
||||
headers?: any
|
||||
): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> {
|
||||
const injected = Utils.injectedDownloads[url]
|
||||
if (injected !== undefined) {
|
||||
|
@ -1067,7 +1067,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
const result = await Utils.downloadAdvanced(
|
||||
url,
|
||||
Utils.Merge({ accept: "application/json" }, headers ?? {}),
|
||||
Utils.Merge({ accept: "application/json" }, headers ?? {})
|
||||
)
|
||||
if (result["error"] !== undefined) {
|
||||
return <{ error: string; url: string; statuscode?: number }>result
|
||||
|
@ -1087,7 +1087,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
"due to",
|
||||
e,
|
||||
"\n",
|
||||
e.stack,
|
||||
e.stack
|
||||
)
|
||||
return { error: "malformed", url }
|
||||
}
|
||||
|
@ -1108,7 +1108,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
| "{gpx=application/gpx+xml}"
|
||||
| "application/json"
|
||||
| "image/png"
|
||||
},
|
||||
}
|
||||
) {
|
||||
const element = document.createElement("a")
|
||||
let file
|
||||
|
@ -1212,7 +1212,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
e.preventDefault()
|
||||
return false
|
||||
},
|
||||
false,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1296,7 +1296,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
public static sortedByLevenshteinDistance<T>(
|
||||
reference: string,
|
||||
ts: T[],
|
||||
getName: (t: T) => string,
|
||||
getName: (t: T) => string
|
||||
): T[] {
|
||||
const withDistance: [T, number][] = ts.map((t) => [
|
||||
t,
|
||||
|
@ -1322,7 +1322,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
track[j][i] = Math.min(
|
||||
track[j][i - 1] + 1, // deletion
|
||||
track[j - 1][i] + 1, // insertion
|
||||
track[j - 1][i - 1] + indicator, // substitution
|
||||
track[j - 1][i - 1] + indicator // substitution
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1331,7 +1331,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
|
||||
public static MapToObj<V, T>(
|
||||
d: Map<string, V>,
|
||||
onValue: (t: V, key: string) => T,
|
||||
onValue: (t: V, key: string) => T
|
||||
): Record<string, T> {
|
||||
const o = {}
|
||||
const keys = Array.from(d.keys())
|
||||
|
@ -1348,7 +1348,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
* Utils.TransposeMap({"a" : ["b", "c"], "x" : ["b", "y"]}) // => {"b" : ["a", "x"], "c" : ["a"], "y" : ["x"]}
|
||||
*/
|
||||
public static TransposeMap<K extends string, V extends string>(
|
||||
d: Record<K, V[]>,
|
||||
d: Record<K, V[]>
|
||||
): Record<V, K[]> {
|
||||
const newD: Record<V, K[]> = <any>{}
|
||||
|
||||
|
@ -1422,7 +1422,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
|
||||
public static asDict(
|
||||
tags: { key: string; value: string | number }[],
|
||||
tags: { key: string; value: string | number }[]
|
||||
): Map<string, string | number> {
|
||||
const d = new Map<string, string | number>()
|
||||
|
||||
|
@ -1529,7 +1529,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
*
|
||||
*/
|
||||
public static splitIntoSubstitutionParts(
|
||||
template: string,
|
||||
template: string
|
||||
): ({ message: string } | { subs: string })[] {
|
||||
const preparts = template.split("{")
|
||||
const spec: ({ message: string } | { subs: string })[] = []
|
||||
|
@ -1591,7 +1591,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
|
||||
public static RemoveDiacritics(str?: string): string {
|
||||
if(!str){
|
||||
if (!str) {
|
||||
return str
|
||||
}
|
||||
return str.normalize("NFD").replace(/\p{Diacritic}/gu, "")
|
||||
|
@ -1638,6 +1638,41 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
return newObj
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a child that can be focused on, by first selecting a 'focusable', then a button, then a link
|
||||
*
|
||||
* Returns the focussed element
|
||||
* @param el
|
||||
*/
|
||||
public static focusOnFocusableChild(el: HTMLElement): undefined {
|
||||
if (!el) {
|
||||
return
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
let childs = el.getElementsByClassName("focusable")
|
||||
if (childs.length == 0) {
|
||||
childs = el.getElementsByTagName("button")
|
||||
if (childs.length === 0) {
|
||||
childs = el.getElementsByTagName("a")
|
||||
}
|
||||
}
|
||||
const child = <HTMLElement>childs.item(0)
|
||||
if (child === null) {
|
||||
console.log("Focussing on child element: no child element found for", el)
|
||||
return undefined
|
||||
}
|
||||
if (
|
||||
child.tagName !== "button" &&
|
||||
child.tagName !== "a" &&
|
||||
child.hasAttribute("tabindex")
|
||||
) {
|
||||
child.setAttribute("tabindex", "-1")
|
||||
}
|
||||
console.log("Focussing on", child)
|
||||
child?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
private static findParentWithScrolling(element: HTMLBaseElement): HTMLBaseElement {
|
||||
// Check if the element itself has scrolling
|
||||
if (element.scrollHeight > element.clientHeight) {
|
||||
|
@ -1655,7 +1690,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
|
||||
private static colorDiff(
|
||||
c0: { r: number; g: number; b: number },
|
||||
c1: { r: number; g: number; b: number },
|
||||
c1: { r: number; g: number; b: number }
|
||||
) {
|
||||
return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b)
|
||||
}
|
||||
|
|
|
@ -70,6 +70,11 @@ body {
|
|||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.focusable {
|
||||
/* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */
|
||||
border: 1px solid red
|
||||
}
|
||||
|
||||
svg,
|
||||
img {
|
||||
box-sizing: content-box;
|
||||
|
@ -159,7 +164,6 @@ input[type=text] {
|
|||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
/******************* Styling of input elements **********************/
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue