forked from MapComplete/MapComplete
		
	First search with suggestions
This commit is contained in:
		
							parent
							
								
									874f82be82
								
							
						
					
					
						commit
						3cd04df60b
					
				
					 37 changed files with 677 additions and 85 deletions
				
			
		|  | @ -45,7 +45,9 @@ | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "id": "profile-title", |       "id": "profile-title", | ||||||
|       "labels": ["hidden"], |       "labels": [ | ||||||
|  |         "hidden" | ||||||
|  |       ], | ||||||
|       "icon": "user_circle", |       "icon": "user_circle", | ||||||
|       "render": { |       "render": { | ||||||
|         "*": "<h3>{_name}</h3>" |         "*": "<h3>{_name}</h3>" | ||||||
|  | @ -63,7 +65,8 @@ | ||||||
|     { |     { | ||||||
|       "id": "profile-description", |       "id": "profile-description", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "profile-content","hidden" |         "profile-content", | ||||||
|  |         "hidden" | ||||||
|       ], |       ], | ||||||
|       "render": { |       "render": { | ||||||
|         "*": "{_description_html}" |         "*": "{_description_html}" | ||||||
|  | @ -71,7 +74,6 @@ | ||||||
|       "mappings": [ |       "mappings": [ | ||||||
|         { |         { | ||||||
|           "if": "_description=", |           "if": "_description=", | ||||||
| 
 |  | ||||||
|           "then": { |           "then": { | ||||||
|             "special": { |             "special": { | ||||||
|               "type": "link", |               "type": "link", | ||||||
|  | @ -98,7 +100,8 @@ | ||||||
|     { |     { | ||||||
|       "id": "edit-profile", |       "id": "edit-profile", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "profile-content","hidden" |         "profile-content", | ||||||
|  |         "hidden" | ||||||
|       ], |       ], | ||||||
|       "condition": "_description!=", |       "condition": "_description!=", | ||||||
|       "render": { |       "render": { | ||||||
|  | @ -126,7 +129,8 @@ | ||||||
|     { |     { | ||||||
|       "id": "verified-mastodon", |       "id": "verified-mastodon", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "profile-content","hidden" |         "profile-content", | ||||||
|  |         "hidden" | ||||||
|       ], |       ], | ||||||
|       "mappings": [ |       "mappings": [ | ||||||
|         { |         { | ||||||
|  | @ -157,7 +161,8 @@ | ||||||
|     { |     { | ||||||
|       "id": "cscount-thanks", |       "id": "cscount-thanks", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "profile-content","hidden" |         "profile-content", | ||||||
|  |         "hidden" | ||||||
|       ], |       ], | ||||||
|       "mappings": [ |       "mappings": [ | ||||||
|         { |         { | ||||||
|  | @ -180,7 +185,8 @@ | ||||||
|     { |     { | ||||||
|       "id": "translation-thanks", |       "id": "translation-thanks", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "profile-content","hidden" |         "profile-content", | ||||||
|  |         "hidden" | ||||||
|       ], |       ], | ||||||
|       "mappings": [ |       "mappings": [ | ||||||
|         { |         { | ||||||
|  | @ -197,7 +203,8 @@ | ||||||
|     { |     { | ||||||
|       "id": "contributor-thanks", |       "id": "contributor-thanks", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "profile-content","hidden" |         "profile-content", | ||||||
|  |         "hidden" | ||||||
|       ], |       ], | ||||||
|       "mappings": [ |       "mappings": [ | ||||||
|         { |         { | ||||||
|  |  | ||||||
|  | @ -8468,6 +8468,13 @@ | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|  |             "edit-profile": { | ||||||
|  |                 "render": { | ||||||
|  |                     "special": { | ||||||
|  |                         "text": "Editeu la descripció del vostre perfil" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "fixate-north": { |             "fixate-north": { | ||||||
|                 "mappings": { |                 "mappings": { | ||||||
|                     "0": { |                     "0": { | ||||||
|  | @ -8529,6 +8536,17 @@ | ||||||
|                 }, |                 }, | ||||||
|                 "question": "Sota quina llicència vols publicar les teves fotos?" |                 "question": "Sota quina llicència vols publicar les teves fotos?" | ||||||
|             }, |             }, | ||||||
|  |             "profile-description": { | ||||||
|  |                 "mappings": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "text": "Afegeix una descripció del perfil" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "settings-link": { |             "settings-link": { | ||||||
|                 "render": { |                 "render": { | ||||||
|                     "special": { |                     "special": { | ||||||
|  |  | ||||||
|  | @ -8723,6 +8723,13 @@ | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|  |             "edit-profile": { | ||||||
|  |                 "render": { | ||||||
|  |                     "special": { | ||||||
|  |                         "text": "Úprava popisu vašeho profilu" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "fixate-north": { |             "fixate-north": { | ||||||
|                 "mappings": { |                 "mappings": { | ||||||
|                     "0": { |                     "0": { | ||||||
|  | @ -8784,6 +8791,17 @@ | ||||||
|                 }, |                 }, | ||||||
|                 "question": "Pod jakou licencí chcete své fotografie zveřejnit?" |                 "question": "Pod jakou licencí chcete své fotografie zveřejnit?" | ||||||
|             }, |             }, | ||||||
|  |             "profile-description": { | ||||||
|  |                 "mappings": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "text": "Přidat popis profilu" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "settings-link": { |             "settings-link": { | ||||||
|                 "render": { |                 "render": { | ||||||
|                     "special": { |                     "special": { | ||||||
|  |  | ||||||
|  | @ -2485,6 +2485,13 @@ | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|  |             "edit-profile": { | ||||||
|  |                 "render": { | ||||||
|  |                     "special": { | ||||||
|  |                         "text": "Ret din profilbeskrivelse" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "fixate-north": { |             "fixate-north": { | ||||||
|                 "mappings": { |                 "mappings": { | ||||||
|                     "0": { |                     "0": { | ||||||
|  |  | ||||||
|  | @ -11119,6 +11119,13 @@ | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|  |             "edit-profile": { | ||||||
|  |                 "render": { | ||||||
|  |                     "special": { | ||||||
|  |                         "text": "Eigene Profilbeschreibung bearbeiten" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "fixate-north": { |             "fixate-north": { | ||||||
|                 "mappings": { |                 "mappings": { | ||||||
|                     "0": { |                     "0": { | ||||||
|  | @ -11207,6 +11214,17 @@ | ||||||
|                 }, |                 }, | ||||||
|                 "question": "Unter welcher Lizenz möchten Sie Ihre Bilder veröffentlichen?" |                 "question": "Unter welcher Lizenz möchten Sie Ihre Bilder veröffentlichen?" | ||||||
|             }, |             }, | ||||||
|  |             "profile-description": { | ||||||
|  |                 "mappings": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "text": "Profilbeschreibung hinzufügen" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "settings-link": { |             "settings-link": { | ||||||
|                 "render": { |                 "render": { | ||||||
|                     "special": { |                     "special": { | ||||||
|  |  | ||||||
|  | @ -11170,6 +11170,13 @@ | ||||||
|             "debug-title": { |             "debug-title": { | ||||||
|                 "render": "<h3>Debugging options</h3>" |                 "render": "<h3>Debugging options</h3>" | ||||||
|             }, |             }, | ||||||
|  |             "edit-profile": { | ||||||
|  |                 "render": { | ||||||
|  |                     "special": { | ||||||
|  |                         "text": "Edit your profile description" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "fixate-north": { |             "fixate-north": { | ||||||
|                 "mappings": { |                 "mappings": { | ||||||
|                     "0": { |                     "0": { | ||||||
|  | @ -11258,6 +11265,17 @@ | ||||||
|                 }, |                 }, | ||||||
|                 "question": "Under what license do you want to publish your pictures?" |                 "question": "Under what license do you want to publish your pictures?" | ||||||
|             }, |             }, | ||||||
|  |             "profile-description": { | ||||||
|  |                 "mappings": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "text": "Add a profile description" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "settings-link": { |             "settings-link": { | ||||||
|                 "render": { |                 "render": { | ||||||
|                     "special": { |                     "special": { | ||||||
|  |  | ||||||
|  | @ -122,6 +122,26 @@ | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "usersettings": { |     "usersettings": { | ||||||
|  |         "tagRenderings": { | ||||||
|  |             "edit-profile": { | ||||||
|  |                 "render": { | ||||||
|  |                     "special": { | ||||||
|  |                         "text": "Muokkaa profiilin kuvausta" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             "profile-description": { | ||||||
|  |                 "mappings": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "text": "Lisää profiilin kuvaus" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "title": { |         "title": { | ||||||
|             "render": "Asetukset" |             "render": "Asetukset" | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -6925,6 +6925,13 @@ | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|  |             "edit-profile": { | ||||||
|  |                 "render": { | ||||||
|  |                     "special": { | ||||||
|  |                         "text": "Modifier ton profil" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "fixate-north": { |             "fixate-north": { | ||||||
|                 "mappings": { |                 "mappings": { | ||||||
|                     "0": { |                     "0": { | ||||||
|  |  | ||||||
|  | @ -842,6 +842,17 @@ | ||||||
|     }, |     }, | ||||||
|     "usersettings": { |     "usersettings": { | ||||||
|         "tagRenderings": { |         "tagRenderings": { | ||||||
|  |             "profile-description": { | ||||||
|  |                 "mappings": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "text": "Legg til profilbeskrivelse" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "translation-completeness": { |             "translation-completeness": { | ||||||
|                 "render": "Oversettelsen for {_theme} i {_language} har {_translation_percentage}% dekning: {_translation_translated_count} strenger av {_translation_total} har blitt oversatt" |                 "render": "Oversettelsen for {_theme} i {_language} har {_translation_percentage}% dekning: {_translation_translated_count} strenger av {_translation_total} har blitt oversatt" | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -8871,6 +8871,13 @@ | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|  |             "edit-profile": { | ||||||
|  |                 "render": { | ||||||
|  |                     "special": { | ||||||
|  |                         "text": "Pas je profielbeschrijving aan" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "fixate-north": { |             "fixate-north": { | ||||||
|                 "mappings": { |                 "mappings": { | ||||||
|                     "0": { |                     "0": { | ||||||
|  | @ -8959,6 +8966,17 @@ | ||||||
|                 }, |                 }, | ||||||
|                 "question": "Met welke licentie wil je je afbeeldingen toevoegen?" |                 "question": "Met welke licentie wil je je afbeeldingen toevoegen?" | ||||||
|             }, |             }, | ||||||
|  |             "profile-description": { | ||||||
|  |                 "mappings": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "text": "Voeg een profielbeschrijving toe" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "settings-link": { |             "settings-link": { | ||||||
|                 "render": { |                 "render": { | ||||||
|                     "special": { |                     "special": { | ||||||
|  |  | ||||||
|  | @ -3338,6 +3338,28 @@ | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|  |     "usersettings": { | ||||||
|  |         "tagRenderings": { | ||||||
|  |             "edit-profile": { | ||||||
|  |                 "render": { | ||||||
|  |                     "special": { | ||||||
|  |                         "text": "Edytuj opis swojego profilu" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             "profile-description": { | ||||||
|  |                 "mappings": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "text": "Dodaj opis profilu" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|     "walls_and_buildings": { |     "walls_and_buildings": { | ||||||
|         "description": "Specjalna warstwa zabudowana zapewniająca wszystkie mury i budynki. Warstwa ta jest przydatna w ustawieniach wstępnych obiektów, które można umieścić przy ścianach (np. AED, skrzynki pocztowe, wejścia, adresy, kamery monitorujące itp.). Warstwa ta jest domyślnie niewidoczna i użytkownik nie może jej przełączać." |         "description": "Specjalna warstwa zabudowana zapewniająca wszystkie mury i budynki. Warstwa ta jest przydatna w ustawieniach wstępnych obiektów, które można umieścić przy ścianach (np. AED, skrzynki pocztowe, wejścia, adresy, kamery monitorujące itp.). Warstwa ta jest domyślnie niewidoczna i użytkownik nie może jej przełączać." | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  | @ -1768,6 +1768,13 @@ | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|  |             "edit-profile": { | ||||||
|  |                 "render": { | ||||||
|  |                     "special": { | ||||||
|  |                         "text": "Editar a descrição do seu perfil" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "picture-license": { |             "picture-license": { | ||||||
|                 "mappings": { |                 "mappings": { | ||||||
|                     "0": { |                     "0": { | ||||||
|  | @ -1785,6 +1792,17 @@ | ||||||
|                 }, |                 }, | ||||||
|                 "question": "Sob que licença você deseja publicar suas fotos?" |                 "question": "Sob que licença você deseja publicar suas fotos?" | ||||||
|             }, |             }, | ||||||
|  |             "profile-description": { | ||||||
|  |                 "mappings": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "text": "Adicionar uma descrição do perfil" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "show_debug": { |             "show_debug": { | ||||||
|                 "mappings": { |                 "mappings": { | ||||||
|                     "0": { |                     "0": { | ||||||
|  |  | ||||||
|  | @ -782,6 +782,24 @@ | ||||||
|     }, |     }, | ||||||
|     "usersettings": { |     "usersettings": { | ||||||
|         "tagRenderings": { |         "tagRenderings": { | ||||||
|  |             "edit-profile": { | ||||||
|  |                 "render": { | ||||||
|  |                     "special": { | ||||||
|  |                         "text": "編輯你的個人檔敘述" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             "profile-description": { | ||||||
|  |                 "mappings": { | ||||||
|  |                     "0": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "text": "新增個人檔敘述" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "translation-completeness": { |             "translation-completeness": { | ||||||
|                 "render": "{_theme} 的 {_language} 翻譯目前是 {_translation_percentage}%:{_translation_total} 中的 {_translation_translated_count} 已經翻譯了" |                 "render": "{_theme} 的 {_language} 翻譯目前是 {_translation_percentage}%:{_translation_total} 中的 {_translation_translated_count} 已經翻譯了" | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  | @ -711,6 +711,14 @@ video { | ||||||
|   top: 2.5rem; |   top: 2.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .top-2 { | ||||||
|  |   top: 0.5rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .right-2 { | ||||||
|  |   right: 0.5rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .left-1\/4 { | .left-1\/4 { | ||||||
|   left: 25%; |   left: 25%; | ||||||
| } | } | ||||||
|  | @ -779,10 +787,6 @@ video { | ||||||
|   top: 0.25rem; |   top: 0.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .top-2 { |  | ||||||
|   top: 0.5rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .top-\[calc\(100\%\+1rem\)\] { | .top-\[calc\(100\%\+1rem\)\] { | ||||||
|   top: calc(100% + 1rem); |   top: calc(100% + 1rem); | ||||||
| } | } | ||||||
|  | @ -1221,14 +1225,14 @@ video { | ||||||
|   height: 6rem; |   height: 6rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .h-full { |  | ||||||
|   height: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .h-screen { | .h-screen { | ||||||
|   height: 100vh; |   height: 100vh; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .h-full { | ||||||
|  |   height: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .h-fit { | .h-fit { | ||||||
|   height: -webkit-fit-content; |   height: -webkit-fit-content; | ||||||
|   height: -moz-fit-content; |   height: -moz-fit-content; | ||||||
|  | @ -1280,6 +1284,10 @@ video { | ||||||
|   height: 2.75rem; |   height: 2.75rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .h-2\/3 { | ||||||
|  |   height: 66.666667%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .h-5 { | .h-5 { | ||||||
|   height: 1.25rem; |   height: 1.25rem; | ||||||
| } | } | ||||||
|  | @ -2043,10 +2051,6 @@ video { | ||||||
|   column-gap: 0px; |   column-gap: 0px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .gap-x-4 { |  | ||||||
|   column-gap: 1rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .gap-y-8 { | .gap-y-8 { | ||||||
|   row-gap: 2rem; |   row-gap: 2rem; | ||||||
| } | } | ||||||
|  | @ -4627,6 +4631,17 @@ button.as-link { | ||||||
|   padding: 0; |   padding: 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | button.unstyled { | ||||||
|  |   background-color: unset; | ||||||
|  |   display: inline-flex; | ||||||
|  |   justify-content: start; | ||||||
|  |   border: none; | ||||||
|  |   border-radius: 0; | ||||||
|  |   box-shadow: none; | ||||||
|  |   margin: 0; | ||||||
|  |   padding: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /******* Other input elements ******/ | /******* Other input elements ******/ | ||||||
| 
 | 
 | ||||||
| .hover-alert:hover { | .hover-alert:hover { | ||||||
|  | @ -5284,6 +5299,11 @@ svg.apply-fill path { | ||||||
|   border-color: rgb(209 213 219 / var(--tw-border-opacity)); |   border-color: rgb(209 213 219 / var(--tw-border-opacity)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .hover\:bg-stone-200:hover { | ||||||
|  |   --tw-bg-opacity: 1; | ||||||
|  |   background-color: rgb(231 229 228 / var(--tw-bg-opacity)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .hover\:bg-indigo-200:hover { | .hover\:bg-indigo-200:hover { | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgb(199 210 254 / var(--tw-bg-opacity)); |   background-color: rgb(199 210 254 / var(--tw-bg-opacity)); | ||||||
|  |  | ||||||
|  | @ -28,10 +28,10 @@ export class SummaryTileSourceRewriter implements FeatureSource { | ||||||
|                 !l.layerDef.id.startsWith("note_import") |                 !l.layerDef.id.startsWith("note_import") | ||||||
|         ) |         ) | ||||||
|         this._summarySource = summarySource |         this._summarySource = summarySource | ||||||
|         filteredLayers.forEach((v, k) => { |         filteredLayers.forEach((v) => { | ||||||
|             v.isDisplayed.addCallback((_) => this.update()) |             v.isDisplayed.addCallback(() => this.update()) | ||||||
|         }) |         }) | ||||||
|         this._summarySource.features.addCallbackAndRunD((_) => this.update()) |         this._summarySource.features.addCallbackAndRunD(() => this.update()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private update() { |     private update() { | ||||||
|  | @ -78,6 +78,9 @@ export class SummaryTileSource extends DynamicTileSource { | ||||||
|             isActive?: Store<boolean> |             isActive?: Store<boolean> | ||||||
|         } |         } | ||||||
|     ) { |     ) { | ||||||
|  |         if(layers.length === 0){ | ||||||
|  |             return | ||||||
|  |         } | ||||||
|         const layersSummed = layers.join("+") |         const layersSummed = layers.join("+") | ||||||
|         const zDiff = 2 |         const zDiff = 2 | ||||||
|         super( |         super( | ||||||
|  |  | ||||||
							
								
								
									
										21
									
								
								src/Logic/Geocoding/CombinedSearcher.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/Logic/Geocoding/CombinedSearcher.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
|  | 
 | ||||||
|  | export default class CombinedSearcher implements GeocodingProvider { | ||||||
|  |     private _providers: ReadonlyArray<GeocodingProvider> | ||||||
|  |     private _providersWithSuggest: ReadonlyArray<GeocodingProvider> | ||||||
|  | 
 | ||||||
|  |     constructor(...providers: ReadonlyArray<GeocodingProvider>) { | ||||||
|  |         this._providers = providers | ||||||
|  |         this._providersWithSuggest = providers.filter(pr => pr.suggest !== undefined) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||||
|  |         const results = await Promise.all(this._providers.map(pr => pr.search(query, options))) | ||||||
|  |         return results.flatMap(x => x) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||||
|  |         const results = await Promise.all(this._providersWithSuggest.map(pr => pr.suggest(query, options))) | ||||||
|  |         return results.flatMap(x => x) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										67
									
								
								src/Logic/Geocoding/CoordinateSearch.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/Logic/Geocoding/CoordinateSearch.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,67 @@ | ||||||
|  | import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
|  | import { Utils } from "../../Utils" | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A simple search-class which interprets possible locations | ||||||
|  |  */ | ||||||
|  | export default class CoordinateSearch implements GeocodingProvider { | ||||||
|  |     private static readonly latLonRegexes: ReadonlyArray<RegExp> = [ | ||||||
|  |         /([0-9]+\.[0-9]+)[ ,;]+([0-9]+\.[0-9]+)/, | ||||||
|  |         /lat:?[ ]*([0-9]+\.[0-9]+)[ ,;]+lon:?[ ]*([0-9]+\.[0-9]+)/, | ||||||
|  |         /https:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([0-9]+\.[0-9]+)\/([0-9]+\.[0-9]+)/, | ||||||
|  |         /https:\/\/www.google.com\/maps\/@([0-9]+.[0-9]+),([0-9]+.[0-9]+).*/ | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     private static readonly lonLatRegexes: ReadonlyArray<RegExp> = [ | ||||||
|  |         /([0-9]+\.[0-9]+)[ ,;]+([0-9]+\.[0-9]+)/ | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * | ||||||
|  |      * @param query | ||||||
|  |      * @param options | ||||||
|  |      * | ||||||
|  |      * const ls = new CoordinateSearch() | ||||||
|  |      * const results = await ls.search("https://www.openstreetmap.org/search?query=Brugge#map=11/51.2611/3.2217") | ||||||
|  |      * results.length // => 1
 | ||||||
|  |      * results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611"}
 | ||||||
|  |      * | ||||||
|  |      * const ls = new CoordinateSearch() | ||||||
|  |      * const results = await ls.search("https://www.openstreetmap.org/#map=11/51.2611/3.2217") | ||||||
|  |      * results.length // => 1
 | ||||||
|  |      * results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611"}
 | ||||||
|  |      * | ||||||
|  |      * const ls = new CoordinateSearch() | ||||||
|  |      * const results = await ls.search("51.2611 3.2217") | ||||||
|  |      * results.length // => 2
 | ||||||
|  |      * results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611"}
 | ||||||
|  |      * results[1] // => {lon: 51.2611, lat: 3.2217, display_name: "lon: 51.2611, lat: 3.2217"}
 | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||||
|  | 
 | ||||||
|  |         const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <GeoCodeResult>{ | ||||||
|  |             lat: Number(m[1]), | ||||||
|  |             lon: Number(m[2]), | ||||||
|  |             display_name: "lon: " + m[2] + ", lat: " + m[1], | ||||||
|  |             source: "coordinateSearch" | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const matchesLonLat =  Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r))) | ||||||
|  |             .map(m => <GeoCodeResult>{ | ||||||
|  |                 lat: Number(m[2]), | ||||||
|  |                 lon: Number(m[1]), | ||||||
|  |                 display_name: "lon: " + m[1] + ", lat: " + m[2], | ||||||
|  |                 source: "coordinateSearch" | ||||||
|  |             }) | ||||||
|  | 
 | ||||||
|  |         return matches.concat(matchesLonLat) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||||
|  |         return this.search(query, options) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										43
									
								
								src/Logic/Geocoding/GeocodingProvider.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/Logic/Geocoding/GeocodingProvider.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | ||||||
|  | import { BBox } from "../BBox" | ||||||
|  | import { Feature, FeatureCollection } from "geojson" | ||||||
|  | 
 | ||||||
|  | export type GeoCodeResult = { | ||||||
|  |     display_name: string | ||||||
|  |     feature?: Feature, | ||||||
|  |     lat: number | ||||||
|  |     lon: number | ||||||
|  |     /** | ||||||
|  |      * Format: | ||||||
|  |      * [lat, lat, lon, lon] | ||||||
|  |      */ | ||||||
|  |     boundingbox?: number[] | ||||||
|  |     osm_type?: "node" | "way" | "relation" | ||||||
|  |     osm_id?: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface GeocodingOptions { | ||||||
|  |     bbox?: BBox, | ||||||
|  |     limit?: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export default interface GeocodingProvider { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @param query | ||||||
|  |      * @param options | ||||||
|  |      */ | ||||||
|  |     suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface ReverseGeocodingProvider { | ||||||
|  |     reverseSearch( | ||||||
|  |         coordinate: { lon: number; lat: number }, | ||||||
|  |         zoom: number, | ||||||
|  |         language?: string | ||||||
|  |     ): Promise<FeatureCollection> ; | ||||||
|  | } | ||||||
|  | 
 | ||||||
							
								
								
									
										86
									
								
								src/Logic/Geocoding/LocalElementSearch.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/Logic/Geocoding/LocalElementSearch.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | ||||||
|  | import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
|  | import ThemeViewState from "../../Models/ThemeViewState" | ||||||
|  | import { Utils } from "../../Utils" | ||||||
|  | import { Feature } from "geojson" | ||||||
|  | import { GeoOperations } from "../GeoOperations" | ||||||
|  | 
 | ||||||
|  | export default class LocalElementSearch implements GeocodingProvider { | ||||||
|  |     private readonly _state: ThemeViewState | ||||||
|  | 
 | ||||||
|  |     constructor(state: ThemeViewState) { | ||||||
|  |         this._state = state | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||||
|  |         return this.searchEntries(query, options, false) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): GeoCodeResult[] { | ||||||
|  |         if (query.length < 3) { | ||||||
|  |             return [] | ||||||
|  |         } | ||||||
|  |         const center: { lon: number; lat: number } = this._state.mapProperties.location.data | ||||||
|  |         const centerPoint: [number, number] = [center.lon, center.lat] | ||||||
|  |         let results: { | ||||||
|  |             feature: Feature, | ||||||
|  |             /** | ||||||
|  |              * Lon, lat | ||||||
|  |              */ | ||||||
|  |             center: [number, number], | ||||||
|  |             levehnsteinD: number, | ||||||
|  |             physicalDistance: number, | ||||||
|  |             searchTerms: string[] | ||||||
|  |         }[] = [] | ||||||
|  |         const properties = this._state.perLayer | ||||||
|  |         query = Utils.simplifyStringForSearch(query) | ||||||
|  |         for (const [_, geoIndexedStore] of properties) { | ||||||
|  |             for (const feature of geoIndexedStore.features.data) { | ||||||
|  |                 const props = feature.properties | ||||||
|  |                 const searchTerms: string[] = Utils.NoNull([props.name, props.alt_name, props.local_name, | ||||||
|  |                     (props["addr:street"] && props["addr:number"]) ? | ||||||
|  |                         props["addr:street"] + props["addr:number"] : undefined]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |                 const levehnsteinD = Math.min(...searchTerms.flatMap(entry => entry.split(/ /)).map(entry => { | ||||||
|  |                     let simplified = Utils.simplifyStringForSearch(entry) | ||||||
|  |                     if (matchStart) { | ||||||
|  |                         simplified = simplified.slice(0, query.length) | ||||||
|  |                     } | ||||||
|  |                     return Utils.levenshteinDistance(query, simplified) | ||||||
|  |                 })) | ||||||
|  |                 const center = GeoOperations.centerpointCoordinates(feature) | ||||||
|  |                 if (levehnsteinD <= 2) { | ||||||
|  |                     results.push({ | ||||||
|  |                         feature, | ||||||
|  |                         center, | ||||||
|  |                         physicalDistance: GeoOperations.distanceBetween(centerPoint, center), | ||||||
|  |                         levehnsteinD, | ||||||
|  |                         searchTerms | ||||||
|  |                     }) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25)) | ||||||
|  |         if (options?.limit) { | ||||||
|  |             results = results.slice(0, options.limit) | ||||||
|  |         } | ||||||
|  |         return results.map(entry => { | ||||||
|  |             const id = entry.feature.properties.id.split("/") | ||||||
|  |             return <GeoCodeResult>{ | ||||||
|  |                 lon: entry.center[0], | ||||||
|  |                 lat: entry.center[1], | ||||||
|  |                 osm_type: id[0], | ||||||
|  |                 osm_id: id[1], | ||||||
|  |                 display_name: entry.searchTerms[0], | ||||||
|  |                 source: "localElementSearch", | ||||||
|  |                 feature: entry.feature | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||||
|  |         return this.searchEntries(query, options, true) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								src/Logic/Geocoding/NominatimGeocoding.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/Logic/Geocoding/NominatimGeocoding.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | ||||||
|  | import { Utils } from "../../Utils" | ||||||
|  | import { BBox } from "../BBox" | ||||||
|  | import Constants from "../../Models/Constants" | ||||||
|  | import { FeatureCollection } from "geojson" | ||||||
|  | import Locale from "../../UI/i18n/Locale" | ||||||
|  | import GeocodingProvider, { GeoCodeResult, ReverseGeocodingProvider } from "./GeocodingProvider" | ||||||
|  | 
 | ||||||
|  | export class NominatimGeocoding implements GeocodingProvider, ReverseGeocodingProvider { | ||||||
|  | 
 | ||||||
|  |     private readonly _host ; | ||||||
|  | 
 | ||||||
|  |     constructor(host: string =  Constants.nominatimEndpoint) { | ||||||
|  |         this._host = host | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<GeoCodeResult[]> { | ||||||
|  |         const b = options?.bbox ?? BBox.global | ||||||
|  |         const url = `${ | ||||||
|  |             this._host | ||||||
|  |         }search?format=json&limit=${options?.limit ?? 1}&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${ | ||||||
|  |             Locale.language.data | ||||||
|  |         }&q=${query}` | ||||||
|  |         return await Utils.downloadJson(url) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     async reverseSearch( | ||||||
|  |         coordinate: { lon: number; lat: number }, | ||||||
|  |         zoom: number = 17, | ||||||
|  |         language?: string | ||||||
|  |     ): Promise<FeatureCollection> { | ||||||
|  |         // https://nominatim.org/release-docs/develop/api/Reverse/
 | ||||||
|  |         // IF the zoom is low, it'll only return a country instead of an address
 | ||||||
|  |         const url = `${this._host}reverse?format=geojson&lat=${coordinate.lat}&lon=${ | ||||||
|  |             coordinate.lon | ||||||
|  |         }&zoom=${Math.ceil(zoom) + 1}&accept-language=${language}` | ||||||
|  |         return Utils.downloadJson(url) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,45 +0,0 @@ | ||||||
| import { Utils } from "../../Utils" |  | ||||||
| import { BBox } from "../BBox" |  | ||||||
| import Constants from "../../Models/Constants" |  | ||||||
| import { FeatureCollection } from "geojson" |  | ||||||
| import Locale from "../../UI/i18n/Locale" |  | ||||||
| 
 |  | ||||||
| export interface GeoCodeResult { |  | ||||||
|     display_name: string |  | ||||||
|     lat: number |  | ||||||
|     lon: number |  | ||||||
|     /** |  | ||||||
|      * Format: |  | ||||||
|      * [lat, lat, lon, lon] |  | ||||||
|      */ |  | ||||||
|     boundingbox: number[] |  | ||||||
|     osm_type: "node" | "way" | "relation" |  | ||||||
|     osm_id: string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export class Geocoding { |  | ||||||
|     public static readonly host = Constants.nominatimEndpoint |  | ||||||
| 
 |  | ||||||
|     static async Search(query: string, bbox: BBox): Promise<GeoCodeResult[]> { |  | ||||||
|         const b = bbox ?? BBox.global |  | ||||||
|         const url = `${ |  | ||||||
|             Geocoding.host |  | ||||||
|         }search?format=json&limit=1&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${ |  | ||||||
|             Locale.language.data |  | ||||||
|         }&q=${query}` |  | ||||||
|         return Utils.downloadJson(url) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static async reverse( |  | ||||||
|         coordinate: { lon: number; lat: number }, |  | ||||||
|         zoom: number = 17, |  | ||||||
|         language?: string |  | ||||||
|     ): Promise<FeatureCollection> { |  | ||||||
|         // https://nominatim.org/release-docs/develop/api/Reverse/
 |  | ||||||
|         // IF the zoom is low, it'll only return a country instead of an address
 |  | ||||||
|         const url = `${Geocoding.host}reverse?format=geojson&lat=${coordinate.lat}&lon=${ |  | ||||||
|             coordinate.lon |  | ||||||
|         }&zoom=${Math.ceil(zoom) + 1}&accept-language=${language}` |  | ||||||
|         return Utils.downloadJson(url) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -29,6 +29,9 @@ export interface MapProperties { | ||||||
|      * @param f |      * @param f | ||||||
|      */ |      */ | ||||||
|     onKeyNavigationEvent(f: (event: KeyNavigationEvent) => void | boolean): () => void |     onKeyNavigationEvent(f: (event: KeyNavigationEvent) => void | boolean): () => void | ||||||
|  | 
 | ||||||
|  |     flyTo(lon: number, lat: number, zoom: number): void | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ExportableMap { | export interface ExportableMap { | ||||||
|  |  | ||||||
|  | @ -1701,7 +1701,8 @@ export class ValidateLayer extends Conversion< | ||||||
|         try { |         try { | ||||||
|             layerConfig = new LayerConfig(json, "validation", true) |             layerConfig = new LayerConfig(json, "validation", true) | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             context.err("Could not parse layer due to:" + e) |             console.error("Could not parse layer due to", e) | ||||||
|  |             context.err("Could not parse layer due to: " + e) | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,11 +19,10 @@ import { Utils } from "../../Utils" | ||||||
| import { TagsFilter } from "../../Logic/Tags/TagsFilter" | import { TagsFilter } from "../../Logic/Tags/TagsFilter" | ||||||
| import FilterConfigJson from "./Json/FilterConfigJson" | import FilterConfigJson from "./Json/FilterConfigJson" | ||||||
| import { Overpass } from "../../Logic/Osm/Overpass" | import { Overpass } from "../../Logic/Osm/Overpass" | ||||||
| import { ImmutableStore } from "../../Logic/UIEventSource" |  | ||||||
| import { OsmTags } from "../OsmFeature" |  | ||||||
| import Constants from "../Constants" | import Constants from "../Constants" | ||||||
| import { QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson" | import { QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson" | ||||||
| import MarkdownUtils from "../../Utils/MarkdownUtils" | import MarkdownUtils from "../../Utils/MarkdownUtils" | ||||||
|  | import Combine from "../../UI/Base/Combine" | ||||||
| 
 | 
 | ||||||
| export default class LayerConfig extends WithContextLoader { | export default class LayerConfig extends WithContextLoader { | ||||||
|     public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const |     public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const | ||||||
|  | @ -344,15 +343,17 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|         this.popupInFloatover = json.popupInFloatover ?? false |         this.popupInFloatover = json.popupInFloatover ?? false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public defaultIcon(): BaseUIElement | undefined { |     public defaultIcon(tags?: Record<string, string>): BaseUIElement | undefined { | ||||||
|         if (this.mapRendering === undefined || this.mapRendering === null) { |         if (this.mapRendering === undefined || this.mapRendering === null) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|         const mapRendering = this.mapRendering.filter((r) => r.location.has("point"))[0] |         const mapRenderings = this.mapRendering.filter((r) => r.location.has("point")) | ||||||
|         if (mapRendering === undefined) { |         if (mapRenderings.length === 0) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|         return mapRendering.GetBaseIcon(this.GetBaseTags()) |         return new Combine(mapRenderings.map( | ||||||
|  |             mr => mr.GetBaseIcon(tags ?? this.GetBaseTags()).SetClass("absolute left-0 top-0 w-full h-full")) | ||||||
|  |         ).SetClass("relative block w-full h-full") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetBaseTags(): Record<string, string> { |     public GetBaseTags(): Record<string, string> { | ||||||
|  |  | ||||||
|  | @ -74,6 +74,11 @@ import Locale from "../UI/i18n/Locale" | ||||||
| import Hash from "../Logic/Web/Hash" | import Hash from "../Logic/Web/Hash" | ||||||
| import { GeoOperations } from "../Logic/GeoOperations" | import { GeoOperations } from "../Logic/GeoOperations" | ||||||
| import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" | import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" | ||||||
|  | import GeocodingProvider from "../Logic/Geocoding/GeocodingProvider" | ||||||
|  | import CombinedSearcher from "../Logic/Geocoding/CombinedSearcher" | ||||||
|  | import { NominatimGeocoding } from "../Logic/Geocoding/NominatimGeocoding" | ||||||
|  | import CoordinateSearch from "../Logic/Geocoding/CoordinateSearch" | ||||||
|  | import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * |  * | ||||||
|  | @ -153,7 +158,8 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|     public readonly visualFeedback: UIEventSource<boolean> = new UIEventSource<boolean>(false) |     public readonly visualFeedback: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||||
|     public readonly toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage> |     public readonly toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage> | ||||||
| 
 | 
 | ||||||
|     public readonly nearbyImageSearcher |     public readonly nearbyImageSearcher: CombinedFetcher | ||||||
|  |     public readonly geosearch: GeocodingProvider | ||||||
| 
 | 
 | ||||||
|     constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) { |     constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) { | ||||||
|         Utils.initDomPurify() |         Utils.initDomPurify() | ||||||
|  | @ -379,6 +385,14 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer", true) |             new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer", true) | ||||||
|         ) |         ) | ||||||
|         this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined |         this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined | ||||||
|  | 
 | ||||||
|  |         this.geosearch = new CombinedSearcher( | ||||||
|  |             new NominatimGeocoding(), | ||||||
|  |             new CoordinateSearch(), | ||||||
|  |             new LocalElementSearch(this) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|         this.initActors() |         this.initActors() | ||||||
|         this.drawSpecialLayers() |         this.drawSpecialLayers() | ||||||
|         this.initHotkeys() |         this.initHotkeys() | ||||||
|  |  | ||||||
|  | @ -4,7 +4,6 @@ | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|   import Loading from "../Base/Loading.svelte" |   import Loading from "../Base/Loading.svelte" | ||||||
|   import Hotkeys from "../Base/Hotkeys" |   import Hotkeys from "../Base/Hotkeys" | ||||||
|   import { Geocoding } from "../../Logic/Osm/Geocoding" |  | ||||||
|   import { BBox } from "../../Logic/BBox" |   import { BBox } from "../../Logic/BBox" | ||||||
|   import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore" |   import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore" | ||||||
|   import { createEventDispatcher, onDestroy } from "svelte" |   import { createEventDispatcher, onDestroy } from "svelte" | ||||||
|  | @ -12,6 +11,12 @@ | ||||||
|   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" |   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||||
|   import { ariaLabel } from "../../Utils/ariaLabel" |   import { ariaLabel } from "../../Utils/ariaLabel" | ||||||
|   import { GeoLocationState } from "../../Logic/State/GeoLocationState" |   import { GeoLocationState } from "../../Logic/State/GeoLocationState" | ||||||
|  |   import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding" | ||||||
|  |   import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|  |   import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|  | 
 | ||||||
|  |   import SearchResults from "./SearchResults.svelte" | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| 
 | 
 | ||||||
|   export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined |   export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined | ||||||
|   export let bounds: UIEventSource<BBox> |   export let bounds: UIEventSource<BBox> | ||||||
|  | @ -19,6 +24,8 @@ | ||||||
| 
 | 
 | ||||||
|   export let geolocationState: GeoLocationState | undefined = undefined |   export let geolocationState: GeoLocationState | undefined = undefined | ||||||
|   export let clearAfterView: boolean = true |   export let clearAfterView: boolean = true | ||||||
|  |   export let searcher : GeocodingProvider = new NominatimGeocoding() | ||||||
|  |   export let state : SpecialVisualizationState | ||||||
|   let searchContents: string = "" |   let searchContents: string = "" | ||||||
|   export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined) |   export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined) | ||||||
|   onDestroy( |   onDestroy( | ||||||
|  | @ -54,6 +61,7 @@ | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|   async function performSearch() { |   async function performSearch() { | ||||||
|     try { |     try { | ||||||
|       isRunning = true |       isRunning = true | ||||||
|  | @ -64,7 +72,8 @@ | ||||||
|       if (searchContents === "") { |       if (searchContents === "") { | ||||||
|         return |         return | ||||||
|       } |       } | ||||||
|       const result = await Geocoding.Search(searchContents, bounds.data) |       const result = await searcher.search(searchContents, { bbox: bounds.data, limit: 10 }) | ||||||
|  |       console.log("Results are", result) | ||||||
|       if (result.length == 0) { |       if (result.length == 0) { | ||||||
|         feedback = Translations.t.general.search.nothing.txt |         feedback = Translations.t.general.search.nothing.txt | ||||||
|         focusOnSearch() |         focusOnSearch() | ||||||
|  | @ -104,6 +113,16 @@ | ||||||
|       isRunning = false |       isRunning = false | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   let suggestions: GeoCodeResult[] = [] | ||||||
|  | 
 | ||||||
|  |   async function updateSuggestions(search){ | ||||||
|  | 
 | ||||||
|  |     suggestions = await searcher.suggest(search, {limit: 5}) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   $: updateSuggestions(searchContents) | ||||||
|  | 
 | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="normal-background flex justify-between rounded-full pl-2"> | <div class="normal-background flex justify-between rounded-full pl-2"> | ||||||
|  | @ -133,3 +152,7 @@ | ||||||
|   </form> |   </form> | ||||||
|   <SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={performSearch} /> |   <SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={performSearch} /> | ||||||
| </div> | </div> | ||||||
|  | 
 | ||||||
|  | <div class="h-2/3 "> | ||||||
|  |   <SearchResults {state} results={suggestions}/> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | @ -60,7 +60,7 @@ export default class MoreScreen { | ||||||
|         if (search === undefined) { |         if (search === undefined) { | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|         search = Utils.RemoveDiacritics(search.toLocaleLowerCase()) |         search = Utils.RemoveDiacritics(search.toLocaleLowerCase()) // See #1729
 | ||||||
|         if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) { |         if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) { | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
|    * Shows the current address when shaken |    * Shows the current address when shaken | ||||||
|    **/ |    **/ | ||||||
|   import Motion from "../../Sensors/Motion" |   import Motion from "../../Sensors/Motion" | ||||||
|   import { Geocoding } from "../../Logic/Osm/Geocoding" |   import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding" | ||||||
|   import Hotkeys from "../Base/Hotkeys" |   import Hotkeys from "../Base/Hotkeys" | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|   import Locale from "../i18n/Locale" |   import Locale from "../i18n/Locale" | ||||||
|  | @ -15,9 +15,11 @@ | ||||||
|   let lastDisplayed: Date = undefined |   let lastDisplayed: Date = undefined | ||||||
|   let currentLocation: string = undefined |   let currentLocation: string = undefined | ||||||
| 
 | 
 | ||||||
|  |   let geocoder = new NominatimGeocoding() | ||||||
|  | 
 | ||||||
|   async function displayLocation() { |   async function displayLocation() { | ||||||
|     lastDisplayed = new Date() |     lastDisplayed = new Date() | ||||||
|     let result = await Geocoding.reverse( |     let result = await geocoder.reverseSearch( | ||||||
|       mapProperties.location.data, |       mapProperties.location.data, | ||||||
|       mapProperties.zoom.data, |       mapProperties.zoom.data, | ||||||
|       Locale.language.data |       Locale.language.data | ||||||
|  |  | ||||||
							
								
								
									
										46
									
								
								src/UI/BigComponents/SearchResult.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/UI/BigComponents/SearchResult.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  |   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|  |   import ToSvelte from "../Base/ToSvelte.svelte" | ||||||
|  |   import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
|  |   import { createEventDispatcher } from "svelte" | ||||||
|  | 
 | ||||||
|  |   export let entry: GeoCodeResult | ||||||
|  |   export let state: SpecialVisualizationState | ||||||
|  |   let layer: LayerConfig | ||||||
|  |   if (entry.feature) { | ||||||
|  |     layer = state.layout.getMatchingLayer(entry.feature.properties) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let dispatch = createEventDispatcher<{select}>() | ||||||
|  |   let distance = state.mapProperties.location.mapD(l => GeoOperations.distanceBetween([l.lon, l.lat], [entry.lon, entry.lat])) | ||||||
|  | 
 | ||||||
|  |   function select() { | ||||||
|  |     state.mapProperties.flyTo(entry.lon, entry.lat, 17) | ||||||
|  |     if (entry.feature) { | ||||||
|  |       state.selectedElement.set(entry.feature) | ||||||
|  |     } | ||||||
|  |     dispatch("select") | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  | <button class="unstyled w-full link-no-underline" | ||||||
|  |         on:click={() => select()}> | ||||||
|  |   <div class="p-2 flex items-center w-full gap-y-2 "> | ||||||
|  | 
 | ||||||
|  |     {#if layer} | ||||||
|  |       <ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} /> | ||||||
|  |     {/if} | ||||||
|  |     <div class="flex flex-col items-start pl-2"> | ||||||
|  |   <div class="flex"> | ||||||
|  | 
 | ||||||
|  |       {entry.display_name ?? entry.osm_id} | ||||||
|  |   </div> | ||||||
|  |       <div class="subtle"> | ||||||
|  |         {#if $distance} | ||||||
|  |           {GeoOperations.distanceToHuman($distance)} | ||||||
|  |         {/if} | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </button> | ||||||
							
								
								
									
										27
									
								
								src/UI/BigComponents/SearchResults.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/UI/BigComponents/SearchResults.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|  |   import SearchResult from "./SearchResult.svelte" | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  |   import { XMarkIcon } from "@babeard/svelte-heroicons/solid" | ||||||
|  | 
 | ||||||
|  |   export let state: SpecialVisualizationState | ||||||
|  |   export let results: GeoCodeResult[] | ||||||
|  | 
 | ||||||
|  |   function close(){ | ||||||
|  |     results = [] | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if results.length > 0} | ||||||
|  |   <div class="relative w-full"> | ||||||
|  | 
 | ||||||
|  |     <div class="absolute top-0 left-0 flex flex-col gap-y-2 normal-background p-2 rounded-xl border border-black w-full"> | ||||||
|  |       {#each results as entry (entry)} | ||||||
|  |         <SearchResult on:select={() => close()} {entry} {state} /> | ||||||
|  |       {/each} | ||||||
|  |     </div> | ||||||
|  |     <div class="absolute top-2 right-2" on:click={() => close()}> | ||||||
|  |       <XMarkIcon class="w-4 h-4 hover:bg-stone-200 rounded-full" /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | {/if} | ||||||
|  | @ -100,6 +100,8 @@ | ||||||
|               {selectedElement} |               {selectedElement} | ||||||
|               {triggerSearch} |               {triggerSearch} | ||||||
|               geolocationState={state.geolocation.geolocationState} |               geolocationState={state.geolocation.geolocationState} | ||||||
|  |               searcher={state.geosearch} | ||||||
|  |               {state} | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|           <button |           <button | ||||||
|  |  | ||||||
|  | @ -679,4 +679,11 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public flyTo(lon: number, lat: number, zoom: number){ | ||||||
|  |         this._maplibreMap.data?.flyTo({ | ||||||
|  |             zoom, | ||||||
|  |             center: [lon, lat], | ||||||
|  |         }) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -104,7 +104,8 @@ | ||||||
|             </div> |             </div> | ||||||
| 
 | 
 | ||||||
|             {#if $reason.includeSearch} |             {#if $reason.includeSearch} | ||||||
|               <Geosearch bounds={currentMapProperties.bounds} clearAfterView={false} /> |               searcher={state.geosearch} | ||||||
|  |               <Geosearch bounds={currentMapProperties.bounds} clearAfterView={false} searcher={state.geosearch} {state}/> | ||||||
|             {/if} |             {/if} | ||||||
| 
 | 
 | ||||||
|             <div class="flex flex-wrap"> |             <div class="flex flex-wrap"> | ||||||
|  |  | ||||||
|  | @ -41,7 +41,7 @@ export interface SpecialVisualizationState { | ||||||
|     readonly layerState: LayerState |     readonly layerState: LayerState | ||||||
|     readonly featureSummary: SummaryTileSourceRewriter |     readonly featureSummary: SummaryTileSourceRewriter | ||||||
|     readonly featureProperties: { |     readonly featureProperties: { | ||||||
|         getStore(id: string): UIEventSource<Record<string, string>> |         getStore(id: string): UIEventSource<Record<string, string>>, | ||||||
|         trackFeature?(feature: { properties: OsmTags }) |         trackFeature?(feature: { properties: OsmTags }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -249,6 +249,8 @@ | ||||||
|           perLayer={state.perLayer} |           perLayer={state.perLayer} | ||||||
|           selectedElement={state.selectedElement} |           selectedElement={state.selectedElement} | ||||||
|           geolocationState={state.geolocation.geolocationState} |           geolocationState={state.geolocation.geolocationState} | ||||||
|  |           searcher={state.geosearch} | ||||||
|  |           {state} | ||||||
|         /> |         /> | ||||||
|       </If> |       </If> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
							
								
								
									
										22
									
								
								src/Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										22
									
								
								src/Utils.ts
									
										
									
									
									
								
							|  | @ -1277,8 +1277,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         return withDistance.map((n) => n[0]) |         return withDistance.map((n) => n[0]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static levenshteinDistance(str1: string, str2: string) { |     public static levenshteinDistance(str1: string, str2: string): number { | ||||||
|         const track = Array(str2.length + 1) |         const track: number[][] = Array(str2.length + 1) | ||||||
|             .fill(null) |             .fill(null) | ||||||
|             .map(() => Array(str1.length + 1).fill(null)) |             .map(() => Array(str1.length + 1).fill(null)) | ||||||
|         for (let i = 0; i <= str1.length; i += 1) { |         for (let i = 0; i <= str1.length; i += 1) { | ||||||
|  | @ -1590,13 +1590,31 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Removes accents from a string | ||||||
|  |      * @param str | ||||||
|  |      * @constructor | ||||||
|  |      * | ||||||
|  |      * Utils.RemoveDiacritics("bâtiments") // => "batiments"
 | ||||||
|  |      */ | ||||||
|     public static RemoveDiacritics(str?: string): string { |     public static RemoveDiacritics(str?: string): string { | ||||||
|  |         // See #1729
 | ||||||
|         if (!str) { |         if (!str) { | ||||||
|             return str |             return str | ||||||
|         } |         } | ||||||
|         return str.normalize("NFD").replace(/\p{Diacritic}/gu, "") |         return str.normalize("NFD").replace(/\p{Diacritic}/gu, "") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Simplifies a string to increase the chance of a match | ||||||
|  |      * @param str | ||||||
|  |      * Utils.simplifyStringForSearch("abc def; ghi 564") // => "abcdefghi564"
 | ||||||
|  |      * Utils.simplifyStringForSearch("âbc déf; ghi 564") // => "abcdefghi564"
 | ||||||
|  |      */ | ||||||
|  |     public static simplifyStringForSearch(str: string): string{ | ||||||
|  |         return Utils.RemoveDiacritics(str) .toLowerCase().replace(/[^a-z0-9]/g, "") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public static randomString(length: number): string { |     public static randomString(length: number): string { | ||||||
|         let result = "" |         let result = "" | ||||||
|         for (let i = 0; i < length; i++) { |         for (let i = 0; i < length; i++) { | ||||||
|  |  | ||||||
|  | @ -207,6 +207,7 @@ button, .button { | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| .group > button { | .group > button { | ||||||
|     padding-right: 1rem !important; /*Flowbite workaround */ |     padding-right: 1rem !important; /*Flowbite workaround */ | ||||||
| } | } | ||||||
|  | @ -276,6 +277,16 @@ button.as-link { | ||||||
|     padding: 0; |     padding: 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | button.unstyled { | ||||||
|  |     background-color: unset; | ||||||
|  |     display: inline-flex; | ||||||
|  |     justify-content: start; | ||||||
|  |     border: none; | ||||||
|  |     box-shadow: none; | ||||||
|  |     margin: 0; | ||||||
|  |     padding: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /******* Other input elements ******/ | /******* Other input elements ******/ | ||||||
| 
 | 
 | ||||||
| .hover-alert:hover { | .hover-alert:hover { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue