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", | ||||
|       "labels": ["hidden"], | ||||
|       "labels": [ | ||||
|         "hidden" | ||||
|       ], | ||||
|       "icon": "user_circle", | ||||
|       "render": { | ||||
|         "*": "<h3>{_name}</h3>" | ||||
|  | @ -63,7 +65,8 @@ | |||
|     { | ||||
|       "id": "profile-description", | ||||
|       "labels": [ | ||||
|         "profile-content","hidden" | ||||
|         "profile-content", | ||||
|         "hidden" | ||||
|       ], | ||||
|       "render": { | ||||
|         "*": "{_description_html}" | ||||
|  | @ -71,7 +74,6 @@ | |||
|       "mappings": [ | ||||
|         { | ||||
|           "if": "_description=", | ||||
| 
 | ||||
|           "then": { | ||||
|             "special": { | ||||
|               "type": "link", | ||||
|  | @ -98,7 +100,8 @@ | |||
|     { | ||||
|       "id": "edit-profile", | ||||
|       "labels": [ | ||||
|         "profile-content","hidden" | ||||
|         "profile-content", | ||||
|         "hidden" | ||||
|       ], | ||||
|       "condition": "_description!=", | ||||
|       "render": { | ||||
|  | @ -126,7 +129,8 @@ | |||
|     { | ||||
|       "id": "verified-mastodon", | ||||
|       "labels": [ | ||||
|         "profile-content","hidden" | ||||
|         "profile-content", | ||||
|         "hidden" | ||||
|       ], | ||||
|       "mappings": [ | ||||
|         { | ||||
|  | @ -157,7 +161,8 @@ | |||
|     { | ||||
|       "id": "cscount-thanks", | ||||
|       "labels": [ | ||||
|         "profile-content","hidden" | ||||
|         "profile-content", | ||||
|         "hidden" | ||||
|       ], | ||||
|       "mappings": [ | ||||
|         { | ||||
|  | @ -180,7 +185,8 @@ | |||
|     { | ||||
|       "id": "translation-thanks", | ||||
|       "labels": [ | ||||
|         "profile-content","hidden" | ||||
|         "profile-content", | ||||
|         "hidden" | ||||
|       ], | ||||
|       "mappings": [ | ||||
|         { | ||||
|  | @ -197,7 +203,8 @@ | |||
|     { | ||||
|       "id": "contributor-thanks", | ||||
|       "labels": [ | ||||
|         "profile-content","hidden" | ||||
|         "profile-content", | ||||
|         "hidden" | ||||
|       ], | ||||
|       "mappings": [ | ||||
|         { | ||||
|  |  | |||
|  | @ -8468,6 +8468,13 @@ | |||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "edit-profile": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|                         "text": "Editeu la descripció del vostre perfil" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "fixate-north": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|  | @ -8529,6 +8536,17 @@ | |||
|                 }, | ||||
|                 "question": "Sota quina llicència vols publicar les teves fotos?" | ||||
|             }, | ||||
|             "profile-description": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "text": "Afegeix una descripció del perfil" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "settings-link": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|  |  | |||
|  | @ -8723,6 +8723,13 @@ | |||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "edit-profile": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|                         "text": "Úprava popisu vašeho profilu" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "fixate-north": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|  | @ -8784,6 +8791,17 @@ | |||
|                 }, | ||||
|                 "question": "Pod jakou licencí chcete své fotografie zveřejnit?" | ||||
|             }, | ||||
|             "profile-description": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "text": "Přidat popis profilu" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "settings-link": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|  |  | |||
|  | @ -2485,6 +2485,13 @@ | |||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "edit-profile": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|                         "text": "Ret din profilbeskrivelse" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "fixate-north": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|  |  | |||
|  | @ -11119,6 +11119,13 @@ | |||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "edit-profile": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|                         "text": "Eigene Profilbeschreibung bearbeiten" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "fixate-north": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|  | @ -11207,6 +11214,17 @@ | |||
|                 }, | ||||
|                 "question": "Unter welcher Lizenz möchten Sie Ihre Bilder veröffentlichen?" | ||||
|             }, | ||||
|             "profile-description": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "text": "Profilbeschreibung hinzufügen" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "settings-link": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|  |  | |||
|  | @ -11170,6 +11170,13 @@ | |||
|             "debug-title": { | ||||
|                 "render": "<h3>Debugging options</h3>" | ||||
|             }, | ||||
|             "edit-profile": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|                         "text": "Edit your profile description" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "fixate-north": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|  | @ -11258,6 +11265,17 @@ | |||
|                 }, | ||||
|                 "question": "Under what license do you want to publish your pictures?" | ||||
|             }, | ||||
|             "profile-description": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "text": "Add a profile description" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "settings-link": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|  |  | |||
|  | @ -122,6 +122,26 @@ | |||
|         } | ||||
|     }, | ||||
|     "usersettings": { | ||||
|         "tagRenderings": { | ||||
|             "edit-profile": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|                         "text": "Muokkaa profiilin kuvausta" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "profile-description": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "text": "Lisää profiilin kuvaus" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "title": { | ||||
|             "render": "Asetukset" | ||||
|         } | ||||
|  |  | |||
|  | @ -6925,6 +6925,13 @@ | |||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "edit-profile": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|                         "text": "Modifier ton profil" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "fixate-north": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|  |  | |||
|  | @ -842,6 +842,17 @@ | |||
|     }, | ||||
|     "usersettings": { | ||||
|         "tagRenderings": { | ||||
|             "profile-description": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "text": "Legg til profilbeskrivelse" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "translation-completeness": { | ||||
|                 "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": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|  | @ -8959,6 +8966,17 @@ | |||
|                 }, | ||||
|                 "question": "Met welke licentie wil je je afbeeldingen toevoegen?" | ||||
|             }, | ||||
|             "profile-description": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "text": "Voeg een profielbeschrijving toe" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "settings-link": { | ||||
|                 "render": { | ||||
|                     "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": { | ||||
|         "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": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|  | @ -1785,6 +1792,17 @@ | |||
|                 }, | ||||
|                 "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": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|  |  | |||
|  | @ -782,6 +782,24 @@ | |||
|     }, | ||||
|     "usersettings": { | ||||
|         "tagRenderings": { | ||||
|             "edit-profile": { | ||||
|                 "render": { | ||||
|                     "special": { | ||||
|                         "text": "編輯你的個人檔敘述" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "profile-description": { | ||||
|                 "mappings": { | ||||
|                     "0": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "text": "新增個人檔敘述" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "translation-completeness": { | ||||
|                 "render": "{_theme} 的 {_language} 翻譯目前是 {_translation_percentage}%:{_translation_total} 中的 {_translation_translated_count} 已經翻譯了" | ||||
|             }, | ||||
|  |  | |||
|  | @ -711,6 +711,14 @@ video { | |||
|   top: 2.5rem; | ||||
| } | ||||
| 
 | ||||
| .top-2 { | ||||
|   top: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .right-2 { | ||||
|   right: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .left-1\/4 { | ||||
|   left: 25%; | ||||
| } | ||||
|  | @ -779,10 +787,6 @@ video { | |||
|   top: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .top-2 { | ||||
|   top: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .top-\[calc\(100\%\+1rem\)\] { | ||||
|   top: calc(100% + 1rem); | ||||
| } | ||||
|  | @ -1221,14 +1225,14 @@ video { | |||
|   height: 6rem; | ||||
| } | ||||
| 
 | ||||
| .h-full { | ||||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| .h-screen { | ||||
|   height: 100vh; | ||||
| } | ||||
| 
 | ||||
| .h-full { | ||||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| .h-fit { | ||||
|   height: -webkit-fit-content; | ||||
|   height: -moz-fit-content; | ||||
|  | @ -1280,6 +1284,10 @@ video { | |||
|   height: 2.75rem; | ||||
| } | ||||
| 
 | ||||
| .h-2\/3 { | ||||
|   height: 66.666667%; | ||||
| } | ||||
| 
 | ||||
| .h-5 { | ||||
|   height: 1.25rem; | ||||
| } | ||||
|  | @ -2043,10 +2051,6 @@ video { | |||
|   column-gap: 0px; | ||||
| } | ||||
| 
 | ||||
| .gap-x-4 { | ||||
|   column-gap: 1rem; | ||||
| } | ||||
| 
 | ||||
| .gap-y-8 { | ||||
|   row-gap: 2rem; | ||||
| } | ||||
|  | @ -4627,6 +4631,17 @@ button.as-link { | |||
|   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 ******/ | ||||
| 
 | ||||
| .hover-alert:hover { | ||||
|  | @ -5284,6 +5299,11 @@ svg.apply-fill path { | |||
|   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 { | ||||
|   --tw-bg-opacity: 1; | ||||
|   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") | ||||
|         ) | ||||
|         this._summarySource = summarySource | ||||
|         filteredLayers.forEach((v, k) => { | ||||
|             v.isDisplayed.addCallback((_) => this.update()) | ||||
|         filteredLayers.forEach((v) => { | ||||
|             v.isDisplayed.addCallback(() => this.update()) | ||||
|         }) | ||||
|         this._summarySource.features.addCallbackAndRunD((_) => this.update()) | ||||
|         this._summarySource.features.addCallbackAndRunD(() => this.update()) | ||||
|     } | ||||
| 
 | ||||
|     private update() { | ||||
|  | @ -78,6 +78,9 @@ export class SummaryTileSource extends DynamicTileSource { | |||
|             isActive?: Store<boolean> | ||||
|         } | ||||
|     ) { | ||||
|         if(layers.length === 0){ | ||||
|             return | ||||
|         } | ||||
|         const layersSummed = layers.join("+") | ||||
|         const zDiff = 2 | ||||
|         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 | ||||
|      */ | ||||
|     onKeyNavigationEvent(f: (event: KeyNavigationEvent) => void | boolean): () => void | ||||
| 
 | ||||
|     flyTo(lon: number, lat: number, zoom: number): void | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export interface ExportableMap { | ||||
|  |  | |||
|  | @ -1701,7 +1701,8 @@ export class ValidateLayer extends Conversion< | |||
|         try { | ||||
|             layerConfig = new LayerConfig(json, "validation", true) | ||||
|         } 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 | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,11 +19,10 @@ import { Utils } from "../../Utils" | |||
| import { TagsFilter } from "../../Logic/Tags/TagsFilter" | ||||
| import FilterConfigJson from "./Json/FilterConfigJson" | ||||
| import { Overpass } from "../../Logic/Osm/Overpass" | ||||
| import { ImmutableStore } from "../../Logic/UIEventSource" | ||||
| import { OsmTags } from "../OsmFeature" | ||||
| import Constants from "../Constants" | ||||
| import { QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson" | ||||
| import MarkdownUtils from "../../Utils/MarkdownUtils" | ||||
| import Combine from "../../UI/Base/Combine" | ||||
| 
 | ||||
| export default class LayerConfig extends WithContextLoader { | ||||
|     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 | ||||
|     } | ||||
| 
 | ||||
|     public defaultIcon(): BaseUIElement | undefined { | ||||
|     public defaultIcon(tags?: Record<string, string>): BaseUIElement | undefined { | ||||
|         if (this.mapRendering === undefined || this.mapRendering === null) { | ||||
|             return undefined | ||||
|         } | ||||
|         const mapRendering = this.mapRendering.filter((r) => r.location.has("point"))[0] | ||||
|         if (mapRendering === undefined) { | ||||
|         const mapRenderings = this.mapRendering.filter((r) => r.location.has("point")) | ||||
|         if (mapRenderings.length === 0) { | ||||
|             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> { | ||||
|  |  | |||
|  | @ -74,6 +74,11 @@ import Locale from "../UI/i18n/Locale" | |||
| import Hash from "../Logic/Web/Hash" | ||||
| import { GeoOperations } from "../Logic/GeoOperations" | ||||
| 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 toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage> | ||||
| 
 | ||||
|     public readonly nearbyImageSearcher | ||||
|     public readonly nearbyImageSearcher: CombinedFetcher | ||||
|     public readonly geosearch: GeocodingProvider | ||||
| 
 | ||||
|     constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) { | ||||
|         Utils.initDomPurify() | ||||
|  | @ -379,6 +385,14 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|             new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer", true) | ||||
|         ) | ||||
|         this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined | ||||
| 
 | ||||
|         this.geosearch = new CombinedSearcher( | ||||
|             new NominatimGeocoding(), | ||||
|             new CoordinateSearch(), | ||||
|             new LocalElementSearch(this) | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|         this.initActors() | ||||
|         this.drawSpecialLayers() | ||||
|         this.initHotkeys() | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ | |||
|   import Translations from "../i18n/Translations" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
|   import Hotkeys from "../Base/Hotkeys" | ||||
|   import { Geocoding } from "../../Logic/Osm/Geocoding" | ||||
|   import { BBox } from "../../Logic/BBox" | ||||
|   import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore" | ||||
|   import { createEventDispatcher, onDestroy } from "svelte" | ||||
|  | @ -12,6 +11,12 @@ | |||
|   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { ariaLabel } from "../../Utils/ariaLabel" | ||||
|   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 bounds: UIEventSource<BBox> | ||||
|  | @ -19,6 +24,8 @@ | |||
| 
 | ||||
|   export let geolocationState: GeoLocationState | undefined = undefined | ||||
|   export let clearAfterView: boolean = true | ||||
|   export let searcher : GeocodingProvider = new NominatimGeocoding() | ||||
|   export let state : SpecialVisualizationState | ||||
|   let searchContents: string = "" | ||||
|   export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined) | ||||
|   onDestroy( | ||||
|  | @ -54,6 +61,7 @@ | |||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   async function performSearch() { | ||||
|     try { | ||||
|       isRunning = true | ||||
|  | @ -64,7 +72,8 @@ | |||
|       if (searchContents === "") { | ||||
|         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) { | ||||
|         feedback = Translations.t.general.search.nothing.txt | ||||
|         focusOnSearch() | ||||
|  | @ -104,6 +113,16 @@ | |||
|       isRunning = false | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   let suggestions: GeoCodeResult[] = [] | ||||
| 
 | ||||
|   async function updateSuggestions(search){ | ||||
| 
 | ||||
|     suggestions = await searcher.suggest(search, {limit: 5}) | ||||
|   } | ||||
| 
 | ||||
|   $: updateSuggestions(searchContents) | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div class="normal-background flex justify-between rounded-full pl-2"> | ||||
|  | @ -133,3 +152,7 @@ | |||
|   </form> | ||||
|   <SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={performSearch} /> | ||||
| </div> | ||||
| 
 | ||||
| <div class="h-2/3 "> | ||||
|   <SearchResults {state} results={suggestions}/> | ||||
| </div> | ||||
|  |  | |||
|  | @ -60,7 +60,7 @@ export default class MoreScreen { | |||
|         if (search === undefined) { | ||||
|             return true | ||||
|         } | ||||
|         search = Utils.RemoveDiacritics(search.toLocaleLowerCase()) | ||||
|         search = Utils.RemoveDiacritics(search.toLocaleLowerCase()) // See #1729
 | ||||
|         if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) { | ||||
|             return true | ||||
|         } | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
|    * Shows the current address when shaken | ||||
|    **/ | ||||
|   import Motion from "../../Sensors/Motion" | ||||
|   import { Geocoding } from "../../Logic/Osm/Geocoding" | ||||
|   import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding" | ||||
|   import Hotkeys from "../Base/Hotkeys" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import Locale from "../i18n/Locale" | ||||
|  | @ -15,9 +15,11 @@ | |||
|   let lastDisplayed: Date = undefined | ||||
|   let currentLocation: string = undefined | ||||
| 
 | ||||
|   let geocoder = new NominatimGeocoding() | ||||
| 
 | ||||
|   async function displayLocation() { | ||||
|     lastDisplayed = new Date() | ||||
|     let result = await Geocoding.reverse( | ||||
|     let result = await geocoder.reverseSearch( | ||||
|       mapProperties.location.data, | ||||
|       mapProperties.zoom.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} | ||||
|               {triggerSearch} | ||||
|               geolocationState={state.geolocation.geolocationState} | ||||
|               searcher={state.geosearch} | ||||
|               {state} | ||||
|             /> | ||||
|           </div> | ||||
|           <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> | ||||
| 
 | ||||
|             {#if $reason.includeSearch} | ||||
|               <Geosearch bounds={currentMapProperties.bounds} clearAfterView={false} /> | ||||
|               searcher={state.geosearch} | ||||
|               <Geosearch bounds={currentMapProperties.bounds} clearAfterView={false} searcher={state.geosearch} {state}/> | ||||
|             {/if} | ||||
| 
 | ||||
|             <div class="flex flex-wrap"> | ||||
|  |  | |||
|  | @ -41,7 +41,7 @@ export interface SpecialVisualizationState { | |||
|     readonly layerState: LayerState | ||||
|     readonly featureSummary: SummaryTileSourceRewriter | ||||
|     readonly featureProperties: { | ||||
|         getStore(id: string): UIEventSource<Record<string, string>> | ||||
|         getStore(id: string): UIEventSource<Record<string, string>>, | ||||
|         trackFeature?(feature: { properties: OsmTags }) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -249,6 +249,8 @@ | |||
|           perLayer={state.perLayer} | ||||
|           selectedElement={state.selectedElement} | ||||
|           geolocationState={state.geolocation.geolocationState} | ||||
|           searcher={state.geosearch} | ||||
|           {state} | ||||
|         /> | ||||
|       </If> | ||||
|     </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]) | ||||
|     } | ||||
| 
 | ||||
|     public static levenshteinDistance(str1: string, str2: string) { | ||||
|         const track = Array(str2.length + 1) | ||||
|     public static levenshteinDistance(str1: string, str2: string): number { | ||||
|         const track: number[][] = Array(str2.length + 1) | ||||
|             .fill(null) | ||||
|             .map(() => Array(str1.length + 1).fill(null)) | ||||
|         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 { | ||||
|         // See #1729
 | ||||
|         if (!str) { | ||||
|             return str | ||||
|         } | ||||
|         return str.normalize("NFD").replace(/\p{Diacritic}/gu, "") | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Simplifies a string to increase the chance of a match | ||||
|      * @param str | ||||
|      * Utils.simplifyStringForSearch("abc def; ghi 564") // => "abcdefghi564"
 | ||||
|      * Utils.simplifyStringForSearch("âbc déf; ghi 564") // => "abcdefghi564"
 | ||||
|      */ | ||||
|     public static simplifyStringForSearch(str: string): string{ | ||||
|         return Utils.RemoveDiacritics(str) .toLowerCase().replace(/[^a-z0-9]/g, "") | ||||
|     } | ||||
| 
 | ||||
|     public static randomString(length: number): string { | ||||
|         let result = "" | ||||
|         for (let i = 0; i < length; i++) { | ||||
|  |  | |||
|  | @ -207,6 +207,7 @@ button, .button { | |||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| .group > button { | ||||
|     padding-right: 1rem !important; /*Flowbite workaround */ | ||||
| } | ||||
|  | @ -276,6 +277,16 @@ button.as-link { | |||
|     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 ******/ | ||||
| 
 | ||||
| .hover-alert:hover { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue