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…
Reference in a new issue