forked from MapComplete/MapComplete
		
	WIP
This commit is contained in:
		
							parent
							
								
									3ab1a0a3f2
								
							
						
					
					
						commit
						617b4854fa
					
				
					 48 changed files with 662 additions and 491 deletions
				
			
		|  | @ -569,7 +569,7 @@ | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           "if": "dog=no", |           "if": "dog=no", | ||||||
|           "icon": "./assets/layers/questions/no_dogs.svg", |           "icon": "\uD83D\uDC15 ⃠", | ||||||
|           "then": { |           "then": { | ||||||
|             "en": "Dogs are <b>not</b> allowed", |             "en": "Dogs are <b>not</b> allowed", | ||||||
|             "nl": "honden zijn <b>niet</b> toegelaten", |             "nl": "honden zijn <b>niet</b> toegelaten", | ||||||
|  |  | ||||||
|  | @ -257,6 +257,7 @@ | ||||||
|     { |     { | ||||||
|       "builtin": "id_presets.shop_types", |       "builtin": "id_presets.shop_types", | ||||||
|       "override": { |       "override": { | ||||||
|  |         "id": "shop_types", | ||||||
|         "labels": [ |         "labels": [ | ||||||
|           "description" |           "description" | ||||||
|         ], |         ], | ||||||
|  | @ -1174,51 +1175,7 @@ | ||||||
|   ], |   ], | ||||||
|   "filter": [ |   "filter": [ | ||||||
|     "open_now", |     "open_now", | ||||||
|     { |     "shop_types", | ||||||
|       "id": "shop-type", |  | ||||||
|       "options": [ |  | ||||||
|         { |  | ||||||
|           "fields": [ |  | ||||||
|             { |  | ||||||
|               "name": "search", |  | ||||||
|               "type": "string" |  | ||||||
|             } |  | ||||||
|           ], |  | ||||||
|           "osmTags": "shop~i~.*{search}.*", |  | ||||||
|           "question": { |  | ||||||
|             "en": "Only show shops selling {search}", |  | ||||||
|             "de": "Nur Geschäfte, die {search} verkaufen", |  | ||||||
|             "nl": "Toon enkel winkels die {search} verkopen", |  | ||||||
|             "es": "Solo mostrar tiendas que vendan {search}", |  | ||||||
|             "fr": "N'afficher que les magasins vendant {search}", |  | ||||||
|             "ca": "Sols mostrar botigues que venen {search}", |  | ||||||
|             "cs": "Zobrazit pouze obchody prodávající {search}" |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       ] |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       "id": "shop-name", |  | ||||||
|       "options": [ |  | ||||||
|         { |  | ||||||
|           "fields": [ |  | ||||||
|             { |  | ||||||
|               "name": "search", |  | ||||||
|               "type": "string" |  | ||||||
|             } |  | ||||||
|           ], |  | ||||||
|           "osmTags": "name~i~.*{search}.*", |  | ||||||
|           "question": { |  | ||||||
|             "en": "Only show shops with name {search}", |  | ||||||
|             "de": "Nur Geschäfte mit dem Namen {search} anzeigen", |  | ||||||
|             "nl": "Toon enkel winkels met naam {search}", |  | ||||||
|             "es": "Solo mostrar tiendas con nombre {search}", |  | ||||||
|             "fr": "N'afficher que les magasins portant le nom {search}", |  | ||||||
|             "cs": "Zobrazit pouze obchody s názvem {search}" |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       ] |  | ||||||
|     }, |  | ||||||
|     { |     { | ||||||
|       "id": "second_hand", |       "id": "second_hand", | ||||||
|       "options": [ |       "options": [ | ||||||
|  |  | ||||||
|  | @ -260,6 +260,7 @@ | ||||||
|         "examples": "Examples", |         "examples": "Examples", | ||||||
|         "fewChangesBefore": "Please, answer a few questions of existing features before adding a new feature.", |         "fewChangesBefore": "Please, answer a few questions of existing features before adding a new feature.", | ||||||
|         "filterPanel": { |         "filterPanel": { | ||||||
|  |             "allTypes": "All types", | ||||||
|             "disableAll": "Disable all", |             "disableAll": "Disable all", | ||||||
|             "enableAll": "Enable all" |             "enableAll": "Enable all" | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|  | @ -6700,15 +6700,6 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "description": "Una botiga", |         "description": "Una botiga", | ||||||
|         "filter": { |  | ||||||
|             "1": { |  | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "Sols mostrar botigues que venen {search}" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "name": "Botiga", |         "name": "Botiga", | ||||||
|         "presets": { |         "presets": { | ||||||
|             "0": { |             "0": { | ||||||
|  |  | ||||||
|  | @ -6979,21 +6979,7 @@ | ||||||
|         }, |         }, | ||||||
|         "description": "Obchod", |         "description": "Obchod", | ||||||
|         "filter": { |         "filter": { | ||||||
|             "1": { |  | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "Zobrazit pouze obchody prodávající {search}" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "2": { |             "2": { | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "Zobrazit pouze obchody s názvem {search}" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "3": { |  | ||||||
|                 "options": { |                 "options": { | ||||||
|                     "0": { |                     "0": { | ||||||
|                         "question": "Zobrazit pouze obchody prodávající použité zboží" |                         "question": "Zobrazit pouze obchody prodávající použité zboží" | ||||||
|  |  | ||||||
|  | @ -1419,6 +1419,13 @@ | ||||||
|                                 "arialabel": "Åbn på openstreetmap.org" |                                 "arialabel": "Åbn på openstreetmap.org" | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|  |                     }, | ||||||
|  |                     "2": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "arialabel": "Åbn på openstreetmap.org" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 "render": { |                 "render": { | ||||||
|  |  | ||||||
|  | @ -5673,6 +5673,13 @@ | ||||||
|                                 "arialabel": "Auf openstreetmap.org öffnen" |                                 "arialabel": "Auf openstreetmap.org öffnen" | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|  |                     }, | ||||||
|  |                     "2": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "arialabel": "Auf openstreetmap.org öffnen" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 "render": { |                 "render": { | ||||||
|  | @ -8855,21 +8862,7 @@ | ||||||
|         }, |         }, | ||||||
|         "description": "Ein Geschäft", |         "description": "Ein Geschäft", | ||||||
|         "filter": { |         "filter": { | ||||||
|             "1": { |  | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "Nur Geschäfte, die {search} verkaufen" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "2": { |             "2": { | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "Nur Geschäfte mit dem Namen {search} anzeigen" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "3": { |  | ||||||
|                 "options": { |                 "options": { | ||||||
|                     "0": { |                     "0": { | ||||||
|                         "question": "Nur Second-Hand-Geschäfte anzeigen" |                         "question": "Nur Second-Hand-Geschäfte anzeigen" | ||||||
|  |  | ||||||
|  | @ -5673,6 +5673,13 @@ | ||||||
|                                 "arialabel": "Open on openstreetmap.org" |                                 "arialabel": "Open on openstreetmap.org" | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|  |                     }, | ||||||
|  |                     "2": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "arialabel": "Open on openstreetmap.org" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 "render": { |                 "render": { | ||||||
|  | @ -8809,6 +8816,14 @@ | ||||||
|             "render": "School <i>{name}</i>" |             "render": "School <i>{name}</i>" | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|  |     "search": { | ||||||
|  |         "description": "Priviliged layer showing the search results", | ||||||
|  |         "tagRenderings": { | ||||||
|  |             "intro": { | ||||||
|  |                 "render": "Search result" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|     "selected_element": { |     "selected_element": { | ||||||
|         "description": "Highlights the currently selected element. Override this layer to have different colors" |         "description": "Highlights the currently selected element. Override this layer to have different colors" | ||||||
|     }, |     }, | ||||||
|  | @ -8855,21 +8870,7 @@ | ||||||
|         }, |         }, | ||||||
|         "description": "A shop", |         "description": "A shop", | ||||||
|         "filter": { |         "filter": { | ||||||
|             "1": { |  | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "Only show shops selling {search}" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "2": { |             "2": { | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "Only show shops with name {search}" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "3": { |  | ||||||
|                 "options": { |                 "options": { | ||||||
|                     "0": { |                     "0": { | ||||||
|                         "question": "Only show shops selling second-hand items" |                         "question": "Only show shops selling second-hand items" | ||||||
|  |  | ||||||
|  | @ -3796,22 +3796,6 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "description": "Una tienda", |         "description": "Una tienda", | ||||||
|         "filter": { |  | ||||||
|             "1": { |  | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "Solo mostrar tiendas que vendan {search}" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "2": { |  | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "Solo mostrar tiendas con nombre {search}" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "name": "Tienda", |         "name": "Tienda", | ||||||
|         "presets": { |         "presets": { | ||||||
|             "0": { |             "0": { | ||||||
|  |  | ||||||
|  | @ -5618,22 +5618,6 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "description": "Un magasin", |         "description": "Un magasin", | ||||||
|         "filter": { |  | ||||||
|             "1": { |  | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "N'afficher que les magasins vendant {search}" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "2": { |  | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "N'afficher que les magasins portant le nom {search}" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "name": "Magasin", |         "name": "Magasin", | ||||||
|         "presets": { |         "presets": { | ||||||
|             "0": { |             "0": { | ||||||
|  |  | ||||||
|  | @ -4664,6 +4664,13 @@ | ||||||
|                                 "arialabel": "Bekijk op openstreetmap.org" |                                 "arialabel": "Bekijk op openstreetmap.org" | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|  |                     }, | ||||||
|  |                     "2": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "arialabel": "Bekijk op openstreetmap.org" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 "render": { |                 "render": { | ||||||
|  | @ -7120,22 +7127,6 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "description": "Een winkel", |         "description": "Een winkel", | ||||||
|         "filter": { |  | ||||||
|             "1": { |  | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "Toon enkel winkels die {search} verkopen" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "2": { |  | ||||||
|                 "options": { |  | ||||||
|                     "0": { |  | ||||||
|                         "question": "Toon enkel winkels met naam {search}" |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "name": "Winkel", |         "name": "Winkel", | ||||||
|         "presets": { |         "presets": { | ||||||
|             "0": { |             "0": { | ||||||
|  |  | ||||||
|  | @ -1948,6 +1948,13 @@ | ||||||
|                                 "arialabel": "Otwórz na openstreetmap.org" |                                 "arialabel": "Otwórz na openstreetmap.org" | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|  |                     }, | ||||||
|  |                     "2": { | ||||||
|  |                         "then": { | ||||||
|  |                             "special": { | ||||||
|  |                                 "arialabel": "Otwórz na openstreetmap.org" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 "render": { |                 "render": { | ||||||
|  |  | ||||||
|  | @ -953,6 +953,11 @@ video { | ||||||
|   margin-right: 1rem; |   margin-right: 1rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .my-8 { | ||||||
|  |   margin-top: 2rem; | ||||||
|  |   margin-bottom: 2rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .-my-1\.5 { | .-my-1\.5 { | ||||||
|   margin-top: -0.375rem; |   margin-top: -0.375rem; | ||||||
|   margin-bottom: -0.375rem; |   margin-bottom: -0.375rem; | ||||||
|  | @ -978,11 +983,6 @@ video { | ||||||
|   margin-right: -0.25rem; |   margin-right: -0.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .my-8 { |  | ||||||
|   margin-top: 2rem; |  | ||||||
|   margin-bottom: 2rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .mx-12 { | .mx-12 { | ||||||
|   margin-left: 3rem; |   margin-left: 3rem; | ||||||
|   margin-right: 3rem; |   margin-right: 3rem; | ||||||
|  | @ -993,10 +993,6 @@ video { | ||||||
|   margin-right: 4rem; |   margin-right: 4rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .mb-4 { |  | ||||||
|   margin-bottom: 1rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .mt-4 { | .mt-4 { | ||||||
|   margin-top: 1rem; |   margin-top: 1rem; | ||||||
| } | } | ||||||
|  | @ -1029,6 +1025,10 @@ video { | ||||||
|   margin-right: 0.25rem; |   margin-right: 0.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .mb-4 { | ||||||
|  |   margin-bottom: 1rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .ml-1 { | .ml-1 { | ||||||
|   margin-left: 0.25rem; |   margin-left: 0.25rem; | ||||||
| } | } | ||||||
|  | @ -1440,10 +1440,6 @@ video { | ||||||
|   max-height: 3rem; |   max-height: 3rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .max-h-96 { |  | ||||||
|   max-height: 24rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .max-h-24 { | .max-h-24 { | ||||||
|   max-height: 6rem; |   max-height: 6rem; | ||||||
| } | } | ||||||
|  | @ -1452,6 +1448,10 @@ video { | ||||||
|   max-height: 16rem; |   max-height: 16rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .max-h-96 { | ||||||
|  |   max-height: 24rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .max-h-full { | .max-h-full { | ||||||
|   max-height: 100%; |   max-height: 100%; | ||||||
| } | } | ||||||
|  | @ -4623,12 +4623,12 @@ button.as-link { | ||||||
|   padding: 0; |   padding: 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| button.unstyled { | button.unstyled, .button-unstyled button { | ||||||
|   background-color: unset; |   background-color: unset; | ||||||
|   display: inline-flex; |   display: inline-flex; | ||||||
|   justify-content: start; |   justify-content: start; | ||||||
|   border: none; |   border: none; | ||||||
|   box-shadow: none; |   box-shadow: none !important; | ||||||
|   margin: 0; |   margin: 0; | ||||||
|   padding: 0; |   padding: 0; | ||||||
| } | } | ||||||
|  | @ -7234,14 +7234,14 @@ svg.apply-fill path { | ||||||
|     width: 50%; |     width: 50%; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .sm\:w-96 { |  | ||||||
|     width: 24rem; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   .sm\:w-11 { |   .sm\:w-11 { | ||||||
|     width: 2.75rem; |     width: 2.75rem; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   .sm\:w-96 { | ||||||
|  |     width: 24rem; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   .sm\:w-auto { |   .sm\:w-auto { | ||||||
|     width: auto; |     width: auto; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { Store, Stores } from "../UIEventSource" | import { Store, Stores } from "../UIEventSource" | ||||||
| 
 | 
 | ||||||
|  | @ -17,12 +17,17 @@ export default class CombinedSearcher implements GeocodingProvider { | ||||||
|      * @param geocoded |      * @param geocoded | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private merge(geocoded: GeoCodeResult[][]): GeoCodeResult[] { |     private merge(geocoded: SearchResult[][]): SearchResult[] { | ||||||
|         const results: GeoCodeResult[] = [] |         const results: SearchResult[] = [] | ||||||
|         const seenIds = new Set<string>() |         const seenIds = new Set<string>() | ||||||
|         for (const geocodedElement of geocoded) { |         for (const geocodedElement of geocoded) { | ||||||
|             for (const entry of geocodedElement) { |             for (const entry of geocodedElement) { | ||||||
|                 const id = entry.osm_type + entry.osm_id | 
 | ||||||
|  | 
 | ||||||
|  |                 if (entry.osm_id === undefined) { | ||||||
|  |                     throw "Invalid search result: a search result always must have an osm_id to be able to merge results from different sources" | ||||||
|  |                 } | ||||||
|  |                 const id = (entry["osm_type"] ?? "") + entry.osm_id | ||||||
|                 if (seenIds.has(id)) { |                 if (seenIds.has(id)) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|  | @ -33,13 +38,14 @@ export default class CombinedSearcher implements GeocodingProvider { | ||||||
|         return results |         return results | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { |     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { | ||||||
|         const results = (await Promise.all(this._providers.map(pr => pr.search(query, options)))) |         const results = (await Promise.all(this._providers.map(pr => pr.search(query, options)))) | ||||||
|         return results.flatMap(x => x) |         return this.merge(results) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> { |     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||||
|         return Stores.concat(this._providersWithSuggest.map(pr => pr.suggest(query, options))) |         return Stores.concat( | ||||||
|  |             this._providersWithSuggest.map(pr => pr.suggest(query, options))) | ||||||
|             .map(gcrss => this.merge(gcrss)) |             .map(gcrss => this.merge(gcrss)) | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider" | import GeocodingProvider, { SearchResult } from "./GeocodingProvider" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { ImmutableStore, Store } from "../UIEventSource" | import { ImmutableStore, Store } from "../UIEventSource" | ||||||
| 
 | 
 | ||||||
|  | @ -52,9 +52,9 @@ export default class CoordinateSearch implements GeocodingProvider { | ||||||
|      * results.length // => 1
 |      * results.length // => 1
 | ||||||
|      * results[0] // => {lat: -57.5802905, lon: -12.7202538, display_name: "lon: -12.7202538, lat: -57.5802905",  "category": "coordinate", "source": "coordinate:latlon"}
 |      * results[0] // => {lat: -57.5802905, lon: -12.7202538, display_name: "lon: -12.7202538, lat: -57.5802905",  "category": "coordinate", "source": "coordinate:latlon"}
 | ||||||
|      */ |      */ | ||||||
|     private directSearch(query: string): GeoCodeResult[] { |     private directSearch(query: string): SearchResult[] { | ||||||
| 
 | 
 | ||||||
|         const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <GeoCodeResult>{ |         const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <SearchResult>{ | ||||||
|             lat: Number(m[1]), |             lat: Number(m[1]), | ||||||
|             lon: Number(m[2]), |             lon: Number(m[2]), | ||||||
|             display_name: "lon: " + m[2] + ", lat: " + m[1], |             display_name: "lon: " + m[2] + ", lat: " + m[1], | ||||||
|  | @ -64,7 +64,7 @@ export default class CoordinateSearch implements GeocodingProvider { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r))) |         const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r))) | ||||||
|             .map(m => <GeoCodeResult>{ |             .map(m => <SearchResult>{ | ||||||
|                 lat: Number(m[2]), |                 lat: Number(m[2]), | ||||||
|                 lon: Number(m[1]), |                 lon: Number(m[1]), | ||||||
|                 display_name: "lon: " + m[1] + ", lat: " + m[2], |                 display_name: "lon: " + m[1] + ", lat: " + m[2], | ||||||
|  | @ -74,11 +74,11 @@ export default class CoordinateSearch implements GeocodingProvider { | ||||||
|         return matches.concat(matchesLonLat) |         return matches.concat(matchesLonLat) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suggest(query: string): Store<GeoCodeResult[]> { |     suggest(query: string): Store<SearchResult[]> { | ||||||
|         return new ImmutableStore(this.directSearch(query)) |         return new ImmutableStore(this.directSearch(query)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search (query: string): Promise<GeoCodeResult[]> { |     async search (query: string): Promise<SearchResult[]> { | ||||||
|         return this.directSearch(query) |         return this.directSearch(query) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										60
									
								
								src/Logic/Geocoding/FilterSearch.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/Logic/Geocoding/FilterSearch.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | ||||||
|  | import { ImmutableStore, Store } from "../UIEventSource" | ||||||
|  | import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider" | ||||||
|  | import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||||
|  | import { Utils } from "../../Utils" | ||||||
|  | import Locale from "../../UI/i18n/Locale" | ||||||
|  | 
 | ||||||
|  | export default class FilterSearch implements GeocodingProvider { | ||||||
|  |     private readonly _state: SpecialVisualizationState | ||||||
|  | 
 | ||||||
|  |     constructor(state: SpecialVisualizationState) { | ||||||
|  |         this._state = state | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { | ||||||
|  |         return [] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private searchDirectly(query: string): SearchResult[] { | ||||||
|  |         const possibleFilters: SearchResult[] = [] | ||||||
|  |         for (const layer of this._state.layout.layers) { | ||||||
|  |             if (!Array.isArray(layer.filters)) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             for (const filter of layer.filters ?? []) { | ||||||
|  |                 for (let i = 0; i < filter.options.length; i++) { | ||||||
|  |                     const option = filter.options[i] | ||||||
|  |                     if (option === undefined) { | ||||||
|  |                         console.log("No options for", filter) | ||||||
|  |                         continue | ||||||
|  |                     } | ||||||
|  |                     const terms = [option.question.txt, | ||||||
|  |                         ...(option.searchTerms?.[Locale.language.data] ?? option.searchTerms?.["en"] ?? [])].flatMap(term => term.split(" ")) | ||||||
|  |                     const levehnsteinD = Math.min(... | ||||||
|  |                         terms.map(entry => { | ||||||
|  |                             const simplified = Utils.simplifyStringForSearch(entry) | ||||||
|  |                             return Utils.levenshteinDistance(query, simplified.slice(0, query.length)) | ||||||
|  |                         })) | ||||||
|  |                     if (levehnsteinD / query.length > 0.25) { | ||||||
|  |                         continue | ||||||
|  |                     } | ||||||
|  |                     possibleFilters.push({ | ||||||
|  |                         payload: { option, layer, filter, index: i }, | ||||||
|  |                         category: "filter", | ||||||
|  |                         osm_id: layer.id + "/" + filter.id + "/" + option.osmTags?.asHumanString() ?? "none", | ||||||
|  |                     }) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return possibleFilters | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||||
|  |         query = Utils.simplifyStringForSearch(query) | ||||||
|  | 
 | ||||||
|  |         return new ImmutableStore(this.searchDirectly(query)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { GeoCodeResult } from "./GeocodingProvider" | import { SearchResult } from "./GeocodingProvider" | ||||||
| import { Store } from "../UIEventSource" | import { Store } from "../UIEventSource" | ||||||
| import { FeatureSource } from "../FeatureSource/FeatureSource" | import { FeatureSource } from "../FeatureSource/FeatureSource" | ||||||
| import { Feature, Geometry } from "geojson" | import { Feature, Geometry } from "geojson" | ||||||
|  | @ -6,7 +6,7 @@ import { Feature, Geometry } from "geojson" | ||||||
| export default class GeocodingFeatureSource implements FeatureSource { | export default class GeocodingFeatureSource implements FeatureSource { | ||||||
|     public features: Store<Feature<Geometry, Record<string, string>>[]> |     public features: Store<Feature<Geometry, Record<string, string>>[]> | ||||||
| 
 | 
 | ||||||
|     constructor(provider: Store<GeoCodeResult[]>) { |     constructor(provider: Store<SearchResult[]>) { | ||||||
|         this.features = provider.map(geocoded => { |         this.features = provider.map(geocoded => { | ||||||
|             if(geocoded === undefined){ |             if(geocoded === undefined){ | ||||||
|                 return [] |                 return [] | ||||||
|  |  | ||||||
|  | @ -5,9 +5,22 @@ import { Store } from "../UIEventSource" | ||||||
| import * as search from "../../assets/generated/layers/search.json" | import * as search from "../../assets/generated/layers/search.json" | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
| import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" | import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" | ||||||
| export type GeocodingCategory = "coordinate" | "city" | "house" | "street" | "locality" | "country" | "train_station" | "county" | "airport" | "shop" | import FilterConfig, { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig" | ||||||
|  | import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| 
 | 
 | ||||||
| export type GeoCodeResult = { | export type GeocodingCategory = | ||||||
|  |     "coordinate" | ||||||
|  |     | "city" | ||||||
|  |     | "house" | ||||||
|  |     | "street" | ||||||
|  |     | "locality" | ||||||
|  |     | "country" | ||||||
|  |     | "train_station" | ||||||
|  |     | "county" | ||||||
|  |     | "airport" | ||||||
|  |     | "shop" | ||||||
|  | 
 | ||||||
|  | export type GeocodeResult =  { | ||||||
|     /** |     /** | ||||||
|      * The name of the feature being displayed |      * The name of the feature being displayed | ||||||
|      */ |      */ | ||||||
|  | @ -25,11 +38,16 @@ export type GeoCodeResult = { | ||||||
|      */ |      */ | ||||||
|     boundingbox?: number[] |     boundingbox?: number[] | ||||||
|     osm_type?: "node" | "way" | "relation" |     osm_type?: "node" | "way" | "relation" | ||||||
|     osm_id?: string, |     osm_id: string, | ||||||
|     category?: GeocodingCategory, |     category?: GeocodingCategory, | ||||||
|     payload?: object, |     payload?: object, | ||||||
|     source?: string |     source?: string | ||||||
| } | } | ||||||
|  | export type FilterPayload = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number } | ||||||
|  | export type SearchResult = | ||||||
|  |     | { category: "filter", osm_id: string, payload:  FilterPayload } | ||||||
|  |     | { category: "theme", osm_id: string, payload: MinimalLayoutInformation } | ||||||
|  |     | GeocodeResult | ||||||
| 
 | 
 | ||||||
| export interface GeocodingOptions { | export interface GeocodingOptions { | ||||||
|     bbox?: BBox, |     bbox?: BBox, | ||||||
|  | @ -40,16 +58,16 @@ export interface GeocodingOptions { | ||||||
| export default interface GeocodingProvider { | export default interface GeocodingProvider { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> |     search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param query |      * @param query | ||||||
|      * @param options |      * @param options | ||||||
|      */ |      */ | ||||||
|     suggest?(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> |     suggest?(query: string, options?: GeocodingOptions): Store<SearchResult[]> | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type ReverseGeocodingResult = Feature<Geometry,{ | export type ReverseGeocodingResult = Feature<Geometry, { | ||||||
|     osm_id: number, |     osm_id: number, | ||||||
|     osm_type: "node" | "way" | "relation", |     osm_type: "node" | "way" | "relation", | ||||||
|     country: string, |     country: string, | ||||||
|  | @ -57,25 +75,26 @@ export type ReverseGeocodingResult = Feature<Geometry,{ | ||||||
|     countrycode: string, |     countrycode: string, | ||||||
|     type: GeocodingCategory, |     type: GeocodingCategory, | ||||||
|     street: string |     street: string | ||||||
| } > | }> | ||||||
| 
 | 
 | ||||||
| export interface ReverseGeocodingProvider { | export interface ReverseGeocodingProvider { | ||||||
|     reverseSearch( |     reverseSearch( | ||||||
|         coordinate: { lon: number; lat: number }, |         coordinate: { lon: number; lat: number }, | ||||||
|         zoom: number, |         zoom: number, | ||||||
|         language?: string |         language?: string, | ||||||
|     ): Promise<ReverseGeocodingResult[]> ; |     ): Promise<ReverseGeocodingResult[]>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class GeocodingUtils { | export class GeocodingUtils { | ||||||
| 
 | 
 | ||||||
|     public static searchLayer =  GeocodingUtils.initSearchLayer() |     public static searchLayer = GeocodingUtils.initSearchLayer() | ||||||
|     private static initSearchLayer():LayerConfig{ | 
 | ||||||
|         if(search["id"] === undefined){ |     private static initSearchLayer(): LayerConfig { | ||||||
|  |         if (search["id"] === undefined) { | ||||||
|             // We are resetting the layeroverview; trying to parse is useless
 |             // We are resetting the layeroverview; trying to parse is useless
 | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|         return new LayerConfig(<LayerConfigJson> search, "search") |         return new LayerConfig(<LayerConfigJson>search, "search") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static categoryToZoomLevel: Record<GeocodingCategory, number> = { |     public static categoryToZoomLevel: Record<GeocodingCategory, number> = { | ||||||
|  | @ -88,7 +107,7 @@ export class GeocodingUtils { | ||||||
|         street: 15, |         street: 15, | ||||||
|         train_station: 14, |         train_station: 14, | ||||||
|         airport: 13, |         airport: 13, | ||||||
|         shop:16 |         shop: 16, | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -103,7 +122,7 @@ export class GeocodingUtils { | ||||||
|         train_station: "train", |         train_station: "train", | ||||||
|         county: "building_office_2", |         county: "building_office_2", | ||||||
|         airport: "airport", |         airport: "airport", | ||||||
|         shop: "building_storefront" |         shop: "building_storefront", | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
| import ThemeViewState from "../../Models/ThemeViewState" | import ThemeViewState from "../../Models/ThemeViewState" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
|  | @ -27,7 +27,7 @@ export default class LocalElementSearch implements GeocodingProvider { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { |     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { | ||||||
|         return this.searchEntries(query, options, false).data |         return this.searchEntries(query, options, false).data | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -41,7 +41,6 @@ export default class LocalElementSearch implements GeocodingProvider { | ||||||
|                     props["addr:street"] + props["addr:number"] : undefined]) |                     props["addr:street"] + props["addr:number"] : undefined]) | ||||||
| 
 | 
 | ||||||
|             let levehnsteinD: number |             let levehnsteinD: number | ||||||
|             console.log("Comparing nearby:", candidateId, props.id) |  | ||||||
|             if (candidateId === props.id) { |             if (candidateId === props.id) { | ||||||
|                 levehnsteinD = 0 |                 levehnsteinD = 0 | ||||||
|             } else { |             } else { | ||||||
|  | @ -54,7 +53,7 @@ export default class LocalElementSearch implements GeocodingProvider { | ||||||
|                 })) |                 })) | ||||||
|             } |             } | ||||||
|             const center = GeoOperations.centerpointCoordinates(feature) |             const center = GeoOperations.centerpointCoordinates(feature) | ||||||
|             if (levehnsteinD <= 2) { |             if ((levehnsteinD / query.length) <= 0.3) { | ||||||
| 
 | 
 | ||||||
|                 let description = "" |                 let description = "" | ||||||
|                 if (feature.properties["addr:street"]) { |                 if (feature.properties["addr:street"]) { | ||||||
|  | @ -76,7 +75,7 @@ export default class LocalElementSearch implements GeocodingProvider { | ||||||
|         return results |         return results | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<GeoCodeResult[]> { |     searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<SearchResult[]> { | ||||||
|         if (query.length < 3) { |         if (query.length < 3) { | ||||||
|             return new ImmutableStore([]) |             return new ImmutableStore([]) | ||||||
|         } |         } | ||||||
|  | @ -101,7 +100,7 @@ export default class LocalElementSearch implements GeocodingProvider { | ||||||
|             } |             } | ||||||
|             return results.map(entry => { |             return results.map(entry => { | ||||||
|                 const [osm_type, osm_id] = entry.feature.properties.id.split("/") |                 const [osm_type, osm_id] = entry.feature.properties.id.split("/") | ||||||
|                 return <GeoCodeResult>{ |                 return <SearchResult>{ | ||||||
|                     lon: entry.center[0], |                     lon: entry.center[0], | ||||||
|                     lat: entry.center[1], |                     lat: entry.center[1], | ||||||
|                     osm_type, |                     osm_type, | ||||||
|  | @ -118,7 +117,7 @@ export default class LocalElementSearch implements GeocodingProvider { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> { |     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||||
|         return this.searchEntries(query, options, true) |         return this.searchEntries(query, options, true) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { BBox } from "../BBox" | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants" | ||||||
| import { FeatureCollection } from "geojson" | import { FeatureCollection } from "geojson" | ||||||
| import Locale from "../../UI/i18n/Locale" | import Locale from "../../UI/i18n/Locale" | ||||||
| import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider" | import GeocodingProvider, { SearchResult } from "./GeocodingProvider" | ||||||
| 
 | 
 | ||||||
| export class NominatimGeocoding implements GeocodingProvider { | export class NominatimGeocoding implements GeocodingProvider { | ||||||
| 
 | 
 | ||||||
|  | @ -13,7 +13,7 @@ export class NominatimGeocoding implements GeocodingProvider { | ||||||
|         this._host = host |         this._host = host | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<GeoCodeResult[]> { |     public search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<SearchResult[]> { | ||||||
|         const b = options?.bbox ?? BBox.global |         const b = options?.bbox ?? BBox.global | ||||||
|         const url = `${ |         const url = `${ | ||||||
|             this._host |             this._host | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { Store, UIEventSource } from "../UIEventSource" | import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
| import { OsmId } from "../../Models/OsmFeature" | import { OsmId } from "../../Models/OsmFeature" | ||||||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||||
| 
 | 
 | ||||||
|  | @ -30,7 +30,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider { | ||||||
|         return undefined |         return undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { |     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { | ||||||
|         const id = OpenStreetMapIdSearch.extractId(query) |         const id = OpenStreetMapIdSearch.extractId(query) | ||||||
|         if (!id) { |         if (!id) { | ||||||
|             return [] |             return [] | ||||||
|  | @ -59,7 +59,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider { | ||||||
|         }] |         }] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suggest?(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> { |     suggest?(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||||
|         return UIEventSource.FromPromise(this.search(query, options)) |         return UIEventSource.FromPromise(this.search(query, options)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants" | ||||||
| import GeocodingProvider, { | import GeocodingProvider, { | ||||||
|     GeoCodeResult, |     GeocodeResult, | ||||||
|     GeocodingCategory, |     GeocodingCategory, | ||||||
|     GeocodingOptions, |     GeocodingOptions, | ||||||
|     ReverseGeocodingProvider, |     ReverseGeocodingProvider, | ||||||
|     ReverseGeocodingResult |     ReverseGeocodingResult, | ||||||
| } from "./GeocodingProvider" | } from "./GeocodingProvider" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { Feature, FeatureCollection } from "geojson" | import { Feature, FeatureCollection } from "geojson" | ||||||
|  | @ -18,7 +18,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | ||||||
|     private static readonly types = { |     private static readonly types = { | ||||||
|         "R": "relation", |         "R": "relation", | ||||||
|         "W": "way", |         "W": "way", | ||||||
|         "N": "node" |         "N": "node", | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -54,7 +54,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> { |     suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> { | ||||||
|         return Stores.FromPromise(this.search(query, options)) |         return Stores.FromPromise(this.search(query, options)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -95,7 +95,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | ||||||
| 
 | 
 | ||||||
|     private getCategory(entry: Feature) { |     private getCategory(entry: Feature) { | ||||||
|         const p = entry.properties |         const p = entry.properties | ||||||
|         if(p.osm_key === "shop"){ |         if (p.osm_key === "shop") { | ||||||
|             return "shop" |             return "shop" | ||||||
|         } |         } | ||||||
|         if (p.osm_value === "train_station" || p.osm_key === "railway") { |         if (p.osm_value === "train_station" || p.osm_key === "railway") { | ||||||
|  | @ -107,7 +107,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | ||||||
|         return p.type |         return p.type | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { |     async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> { | ||||||
|         if (query.length < 3) { |         if (query.length < 3) { | ||||||
|             return [] |             return [] | ||||||
|         } |         } | ||||||
|  | @ -126,7 +126,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | ||||||
|                 const [lon0, lat0, lon1, lat1] = f.properties.extent |                 const [lon0, lat0, lon1, lat1] = f.properties.extent | ||||||
|                 boundingbox = [lat0, lat1, lon0, lon1] |                 boundingbox = [lat0, lat1, lon0, lon1] | ||||||
|             } |             } | ||||||
|             return <GeoCodeResult>{ |             return <GeocodeResult>{ | ||||||
|                 feature: f, |                 feature: f, | ||||||
|                 osm_id: f.properties.osm_id, |                 osm_id: f.properties.osm_id, | ||||||
|                 display_name: f.properties.name, |                 display_name: f.properties.name, | ||||||
|  | @ -135,7 +135,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | ||||||
|                 category: this.getCategory(f), |                 category: this.getCategory(f), | ||||||
|                 boundingbox, |                 boundingbox, | ||||||
|                 lon, lat, |                 lon, lat, | ||||||
|                 source: this._endpoint |                 source: this._endpoint, | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,36 +1,41 @@ | ||||||
| import { Store, UIEventSource } from "../UIEventSource" | import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import { OsmConnection } from "../Osm/OsmConnection" | import { OsmConnection } from "../Osm/OsmConnection" | ||||||
| import { GeoCodeResult } from "./GeocodingProvider" | import { GeocodeResult } from "./GeocodingProvider" | ||||||
| import { GeoOperations } from "../GeoOperations" | import { GeoOperations } from "../GeoOperations" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| 
 | 
 | ||||||
| export class RecentSearch { | export class RecentSearch { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     private readonly _seenThisSession: UIEventSource<GeoCodeResult[]> |     private readonly _seenThisSession: UIEventSource<GeocodeResult[]> | ||||||
|     public readonly seenThisSession: Store<GeoCodeResult[]> |     public readonly seenThisSession: Store<GeocodeResult[]> | ||||||
| 
 | 
 | ||||||
|     constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) { |     constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) { | ||||||
|         const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches") |         const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches") | ||||||
|         prefs.addCallbackAndRunD(prev => console.trace("Previous searches are:", prev)) |  | ||||||
|         prefs.set(null) |         prefs.set(null) | ||||||
|         this._seenThisSession =  new UIEventSource<GeoCodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
 |         this._seenThisSession = new UIEventSource<GeocodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
 | ||||||
|         this.seenThisSession = this._seenThisSession |         this.seenThisSession = this._seenThisSession | ||||||
| 
 | 
 | ||||||
|         prefs.addCallbackAndRunD(prefs => { |         prefs.addCallbackAndRunD(pref => { | ||||||
|             if(prefs === ""){ |             if (pref === "") { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             const simpleArr = <GeoCodeResult[]> JSON.parse(prefs) |             try { | ||||||
|             if(simpleArr.length > 0){ | 
 | ||||||
|                 this._seenThisSession.set(simpleArr) |                 const simpleArr = <GeocodeResult[]>JSON.parse(pref) | ||||||
|                 return true |                 if (simpleArr.length > 0) { | ||||||
|  |                     this._seenThisSession.set(simpleArr) | ||||||
|  |                     return true | ||||||
|  |                 } | ||||||
|  |             } catch (e) { | ||||||
|  |                 console.error(e, pref) | ||||||
|  |                 prefs.setData("") | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         this.seenThisSession.stabilized(2500).addCallbackAndRunD(seen => { |         this.seenThisSession.stabilized(2500).addCallbackAndRunD(seen => { | ||||||
|             const results=  [] |             const results = [] | ||||||
|             for (let i = 0; i < Math.min(3, seen.length); i++) { |             for (let i = 0; i < Math.min(3, seen.length); i++) { | ||||||
|                 const gc = seen[i] |                 const gc = seen[i] | ||||||
|                 const simple = { |                 const simple = { | ||||||
|  | @ -39,7 +44,7 @@ export class RecentSearch { | ||||||
|                     display_name: gc.display_name, |                     display_name: gc.display_name, | ||||||
|                     lat: gc.lat, lon: gc.lon, |                     lat: gc.lat, lon: gc.lon, | ||||||
|                     osm_id: gc.osm_id, |                     osm_id: gc.osm_id, | ||||||
|                     osm_type: gc.osm_type |                     osm_type: gc.osm_type, | ||||||
|                 } |                 } | ||||||
|                 results.push(simple) |                 results.push(simple) | ||||||
|             } |             } | ||||||
|  | @ -51,25 +56,25 @@ export class RecentSearch { | ||||||
|         state.selectedElement.addCallbackAndRunD(selected => { |         state.selectedElement.addCallbackAndRunD(selected => { | ||||||
| 
 | 
 | ||||||
|             const [osm_type, osm_id] = selected.properties.id.split("/") |             const [osm_type, osm_id] = selected.properties.id.split("/") | ||||||
|             if(!osm_id){ |             if (!osm_id) { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             console.log("Selected element is", selected) |             console.log("Selected element is", selected) | ||||||
|             if(["node","way","relation"].indexOf(osm_type) < 0){ |             if (["node", "way", "relation"].indexOf(osm_type) < 0) { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             const [lon, lat] = GeoOperations.centerpointCoordinates(selected) |             const [lon, lat] = GeoOperations.centerpointCoordinates(selected) | ||||||
|             const entry = <GeoCodeResult>{ |             const entry = <GeocodeResult>{ | ||||||
|                 feature: selected, |                 feature: selected, | ||||||
|                 osm_id, osm_type, |                 osm_id, osm_type, | ||||||
|                 lon, lat |                 lon, lat, | ||||||
|             } |             } | ||||||
|             this.addSelected(entry) |             this.addSelected(entry) | ||||||
| 
 | 
 | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addSelected(entry: GeoCodeResult) { |     addSelected(entry: GeocodeResult) { | ||||||
|         const arr = [...(this.seenThisSession.data ?? []).slice(0, 20), entry] |         const arr = [...(this.seenThisSession.data ?? []).slice(0, 20), entry] | ||||||
| 
 | 
 | ||||||
|         const seenIds = new Set<string>() |         const seenIds = new Set<string>() | ||||||
|  | @ -81,7 +86,7 @@ export class RecentSearch { | ||||||
|                 seenIds.add(id) |                 seenIds.add(id) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         console.log(">>>",arr) |         console.log(">>>", arr) | ||||||
|         this._seenThisSession.set(arr) |         this._seenThisSession.set(arr) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
| import * as themeOverview from "../../assets/generated/theme_overview.json" | import * as themeOverview from "../../assets/generated/theme_overview.json" | ||||||
| import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||||
|  | @ -17,15 +17,15 @@ export default class ThemeSearch implements GeocodingProvider { | ||||||
|         this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection) |         this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { |     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { | ||||||
|         return this.searchDirect(query, options) |         return this.searchDirect(query, options) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> { |     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||||
|         return new ImmutableStore(this.searchDirect(query, options)) |         return new ImmutableStore(this.searchDirect(query, options)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private searchDirect(query: string, options?: GeocodingOptions): GeoCodeResult[] { |     private searchDirect(query: string, options?: GeocodingOptions): SearchResult[] { | ||||||
|         if(query.length < 1){ |         if(query.length < 1){ | ||||||
|             return [] |             return [] | ||||||
|         } |         } | ||||||
|  | @ -37,8 +37,9 @@ export default class ThemeSearch implements GeocodingProvider { | ||||||
|             .filter(th => MoreScreen.MatchesLayout(th, query)) |             .filter(th => MoreScreen.MatchesLayout(th, query)) | ||||||
|             .slice(0, limit + 1) |             .slice(0, limit + 1) | ||||||
| 
 | 
 | ||||||
|         return withMatch.map(match => <GeoCodeResult> { |         return withMatch.map(match => <SearchResult> { | ||||||
|             payload: match, |             payload: match, | ||||||
|  |             category: "theme", | ||||||
|             osm_id: match.id |             osm_id: match.id | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -7,7 +7,13 @@ import { Tag } from "../Tags/Tag" | ||||||
| import Translations from "../../UI/i18n/Translations" | import Translations from "../../UI/i18n/Translations" | ||||||
| import { RegexTag } from "../Tags/RegexTag" | import { RegexTag } from "../Tags/RegexTag" | ||||||
| import { Or } from "../Tags/Or" | import { Or } from "../Tags/Or" | ||||||
|  | import FilterConfig from "../../Models/ThemeConfig/FilterConfig" | ||||||
| 
 | 
 | ||||||
|  | export type ActiveFilter = { | ||||||
|  |     layer: LayerConfig, | ||||||
|  |     filter: FilterConfig, | ||||||
|  |     control: UIEventSource<string | number | undefined> | ||||||
|  | } | ||||||
| /** | /** | ||||||
|  * The layer state keeps track of: |  * The layer state keeps track of: | ||||||
|  * - Which layers are enabled |  * - Which layers are enabled | ||||||
|  | @ -26,6 +32,9 @@ export default class LayerState { | ||||||
|      * Which layers are enabled in the current theme and what filters are applied onto them |      * Which layers are enabled in the current theme and what filters are applied onto them | ||||||
|      */ |      */ | ||||||
|     public readonly filteredLayers: ReadonlyMap<string, FilteredLayer> |     public readonly filteredLayers: ReadonlyMap<string, FilteredLayer> | ||||||
|  |     private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([]) | ||||||
|  | 
 | ||||||
|  |     public readonly activeFilters: Store<ActiveFilter[]> = this._activeFilters | ||||||
|     private readonly osmConnection: OsmConnection |     private readonly osmConnection: OsmConnection | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -56,6 +65,41 @@ export default class LayerState { | ||||||
|         } |         } | ||||||
|         this.filteredLayers = filteredLayers |         this.filteredLayers = filteredLayers | ||||||
|         layers.forEach((l) => LayerState.linkFilterStates(l, filteredLayers)) |         layers.forEach((l) => LayerState.linkFilterStates(l, filteredLayers)) | ||||||
|  | 
 | ||||||
|  |         this.filteredLayers.forEach(fl => { | ||||||
|  |             fl.isDisplayed.addCallback(() => this.updateActiveFilters()) | ||||||
|  |             for (const [_, appliedFilter] of fl.appliedFilters) { | ||||||
|  |                 appliedFilter.addCallback(() => this.updateActiveFilters()) | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |         this.updateActiveFilters() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private updateActiveFilters(){ | ||||||
|  |         const filters: ActiveFilter[] = [] | ||||||
|  |         this.filteredLayers.forEach(fl => { | ||||||
|  |             if(!fl.isDisplayed.data){ | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             for (const [filtername, appliedFilter] of fl.appliedFilters) { | ||||||
|  |                 if (appliedFilter.data === undefined) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 const filter = fl.layerDef.filters.find(f => f.id === filtername) | ||||||
|  |                 if(typeof appliedFilter.data === "number"){ | ||||||
|  |                     if(filter.options[appliedFilter.data].osmTags === undefined){ | ||||||
|  |                         // This is probably the first, generic option which doesn't _actually_ filter
 | ||||||
|  |                         continue | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 filters.push({ | ||||||
|  |                     layer: fl.layerDef, | ||||||
|  |                     control: appliedFilter, | ||||||
|  |                     filter, | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |         this._activeFilters.set(filters) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -207,7 +207,6 @@ export default class UserRelatedState { | ||||||
|         isOfficial: boolean |         isOfficial: boolean | ||||||
|     } |     } | ||||||
|         | undefined { |         | undefined { | ||||||
|         console.log("GETTING UNOFFICIAL THEME") |  | ||||||
|         const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) |         const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) | ||||||
|         const str = pref.data |         const str = pref.data | ||||||
| 
 | 
 | ||||||
|  | @ -351,10 +350,8 @@ export default class UserRelatedState { | ||||||
|                     const key = k.substring(0, k.length - "length".length) |                     const key = k.substring(0, k.length - "length".length) | ||||||
|                     let combined = "" |                     let combined = "" | ||||||
|                     for (let i = 0; i < l; i++) { |                     for (let i = 0; i < l; i++) { | ||||||
|                         console.log("Building preference:",key,i,">>>", newPrefs[key + i], "<<<", newPrefs, ) |  | ||||||
|                         combined += (newPrefs[key + i]) |                         combined += (newPrefs[key + i]) | ||||||
|                     } |                     } | ||||||
|                     console.log("Combined",key,">>>",combined) |  | ||||||
|                     amendedPrefs.data[key.substring(0, key.length - "-combined-".length)] = combined |                     amendedPrefs.data[key.substring(0, key.length - "-combined-".length)] = combined | ||||||
|                 } else { |                 } else { | ||||||
|                     amendedPrefs.data[k] = newPrefs[k] |                     amendedPrefs.data[k] = newPrefs[k] | ||||||
|  |  | ||||||
|  | @ -33,6 +33,7 @@ import LineRenderingConfigJson from "../Json/LineRenderingConfigJson" | ||||||
| import { ConversionContext } from "./ConversionContext" | import { ConversionContext } from "./ConversionContext" | ||||||
| import { ExpandRewrite } from "./ExpandRewrite" | import { ExpandRewrite } from "./ExpandRewrite" | ||||||
| import { TagUtils } from "../../../Logic/Tags/TagUtils" | import { TagUtils } from "../../../Logic/Tags/TagUtils" | ||||||
|  | import { Translatable } from "../Json/Translatable" | ||||||
| 
 | 
 | ||||||
| class ExpandFilter extends DesugaringStep<LayerConfigJson> { | class ExpandFilter extends DesugaringStep<LayerConfigJson> { | ||||||
|     private static readonly predefinedFilters = ExpandFilter.load_filters() |     private static readonly predefinedFilters = ExpandFilter.load_filters() | ||||||
|  | @ -40,7 +41,7 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> { | ||||||
| 
 | 
 | ||||||
|     constructor(state: DesugaringContext) { |     constructor(state: DesugaringContext) { | ||||||
|         super( |         super( | ||||||
|             "Expands filters: replaces a shorthand by the value found in 'filters.json'. If the string is formatted 'layername.filtername, it will be looked up into that layer instead", |             "Expands filters: replaces a shorthand by the value found in 'filters.json'. If the string is formatted 'layername.filtername, it will be looked up into that layer instead. If a tagRendering sets 'filter', this filter will also be included", | ||||||
|             ["filter"], |             ["filter"], | ||||||
|             "ExpandFilter", |             "ExpandFilter", | ||||||
|         ) |         ) | ||||||
|  | @ -67,6 +68,9 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> { | ||||||
|         const newFilters: FilterConfigJson[] = [] |         const newFilters: FilterConfigJson[] = [] | ||||||
|         const filters = <(FilterConfigJson | string)[]>json.filter |         const filters = <(FilterConfigJson | string)[]>json.filter | ||||||
| 
 | 
 | ||||||
|  |         /** | ||||||
|  |          * Checks all tagRendering. If a tagrendering has 'filter' set, add this filter to the layer config | ||||||
|  |          */ | ||||||
|         for (let i = 0; i < json.tagRenderings?.length; i++) { |         for (let i = 0; i < json.tagRenderings?.length; i++) { | ||||||
|             const tagRendering = <TagRenderingConfigJson>json.tagRenderings[i] |             const tagRendering = <TagRenderingConfigJson>json.tagRenderings[i] | ||||||
|             if (!tagRendering?.filter) { |             if (!tagRendering?.filter) { | ||||||
|  | @ -94,6 +98,9 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         /** | ||||||
|  |          * Create filters based on builtin filters | ||||||
|  |          */ | ||||||
|         for (let i = 0; i < filters.length; i++) { |         for (let i = 0; i < filters.length; i++) { | ||||||
|             const filter = filters[i] |             const filter = filters[i] | ||||||
|             if (filter === undefined) { |             if (filter === undefined) { | ||||||
|  | @ -115,15 +122,16 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> { | ||||||
|                             "Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings", |                             "Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings", | ||||||
|                         ) |                         ) | ||||||
|                 } |                 } | ||||||
|                 const options = matchingTr.mappings.map((mapping) => ({ |                 const options = (<QuestionableTagRenderingConfigJson> matchingTr).mappings.map((mapping) => ({ | ||||||
|                     question: mapping.then, |                     question: mapping.then, | ||||||
|                     osmTags: mapping.if, |                     osmTags: mapping.if, | ||||||
|  |                     searchTerms: mapping.searchTerms | ||||||
|  | 
 | ||||||
|                 })) |                 })) | ||||||
|                 options.unshift({ |                 options.unshift({ | ||||||
|                     question: matchingTr["question"] ?? { |                     question: matchingTr["question"] ?? Translations.t.general.filterPanel.allTypes, | ||||||
|                         en: "All types", |  | ||||||
|                     }, |  | ||||||
|                     osmTags: undefined, |                     osmTags: undefined, | ||||||
|  |                     searchTerms: undefined | ||||||
|                 }) |                 }) | ||||||
|                 newFilters.push({ |                 newFilters.push({ | ||||||
|                     id: filter, |                     id: filter, | ||||||
|  |  | ||||||
|  | @ -103,6 +103,10 @@ export class DoesImageExist extends DesugaringStep<string> { | ||||||
|             return image |             return image | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if(Utils.isEmoji(image)){ | ||||||
|  |             return image | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         if (!this._knownImagePaths.has(image)) { |         if (!this._knownImagePaths.has(image)) { | ||||||
|             if (this.doesPathExist === undefined) { |             if (this.doesPathExist === undefined) { | ||||||
|                 context.err( |                 context.err( | ||||||
|  |  | ||||||
|  | @ -8,12 +8,12 @@ import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import { QueryParameters } from "../../Logic/Web/QueryParameters" | import { QueryParameters } from "../../Logic/Web/QueryParameters" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { RegexTag } from "../../Logic/Tags/RegexTag" | import { RegexTag } from "../../Logic/Tags/RegexTag" | ||||||
| import BaseUIElement from "../../UI/BaseUIElement" |  | ||||||
| import Table from "../../UI/Base/Table" |  | ||||||
| import Combine from "../../UI/Base/Combine" |  | ||||||
| import MarkdownUtils from "../../Utils/MarkdownUtils" | import MarkdownUtils from "../../Utils/MarkdownUtils" | ||||||
|  | 
 | ||||||
| export type FilterConfigOption = { | export type FilterConfigOption = { | ||||||
|     question: Translation |     question: Translation | ||||||
|  |     searchTerms: Record<string, string[]> | ||||||
|  |     icon?: string | ||||||
|     osmTags: TagsFilter | undefined |     osmTags: TagsFilter | undefined | ||||||
|     /* Only set if fields are present. Used to create `osmTags` (which are used to _actually_ filter) when the field is written*/ |     /* Only set if fields are present. Used to create `osmTags` (which are used to _actually_ filter) when the field is written*/ | ||||||
|     readonly originalTagsSpec: TagConfigJson |     readonly originalTagsSpec: TagConfigJson | ||||||
|  | @ -105,8 +105,10 @@ export default class FilterConfig { | ||||||
|             return { |             return { | ||||||
|                 question: question, |                 question: question, | ||||||
|                 osmTags: osmTags, |                 osmTags: osmTags, | ||||||
|  |                 searchTerms: option.searchTerms, | ||||||
|                 fields, |                 fields, | ||||||
|                 originalTagsSpec: option.osmTags, |                 originalTagsSpec: option.osmTags, | ||||||
|  |                 icon: option.icon | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  | @ -151,7 +153,7 @@ export default class FilterConfig { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public initState(layerId: string): UIEventSource<undefined | number | string> { |     public initState(layerId: string): UIEventSource<undefined | number | string> { | ||||||
|         let defaultValue = "" |         let defaultValue: string | ||||||
|         if (this.options.length > 1) { |         if (this.options.length > 1) { | ||||||
|             defaultValue = "" + (this.defaultSelection ?? 0) |             defaultValue = "" + (this.defaultSelection ?? 0) | ||||||
|         } else if (this.options[0].fields?.length > 0) { |         } else if (this.options[0].fields?.length > 0) { | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { TagConfigJson } from "./TagConfigJson" | import { TagConfigJson } from "./TagConfigJson" | ||||||
|  | import { Translatable } from "./Translatable" | ||||||
| 
 | 
 | ||||||
| export default interface FilterConfigJson { | export default interface FilterConfigJson { | ||||||
|     /** |     /** | ||||||
|  | @ -34,7 +35,9 @@ export default interface FilterConfigJson { | ||||||
|      *     ``` |      *     ``` | ||||||
|      */ |      */ | ||||||
|     options: { |     options: { | ||||||
|         question: string | any |         question: Translatable | ||||||
|  |         searchTerms?: Record<string, string[]> | ||||||
|  |         icon?: string | ||||||
|         osmTags?: TagConfigJson |         osmTags?: TagConfigJson | ||||||
|         default?: boolean |         default?: boolean | ||||||
|         fields?: { |         fields?: { | ||||||
|  |  | ||||||
|  | @ -76,6 +76,7 @@ import { RecentSearch } from "../Logic/Geocoding/RecentSearch" | ||||||
| import PhotonSearch from "../Logic/Geocoding/PhotonSearch" | import PhotonSearch from "../Logic/Geocoding/PhotonSearch" | ||||||
| import ThemeSearch from "../Logic/Geocoding/ThemeSearch" | import ThemeSearch from "../Logic/Geocoding/ThemeSearch" | ||||||
| import OpenStreetMapIdSearch from "../Logic/Geocoding/OpenStreetMapIdSearch" | import OpenStreetMapIdSearch from "../Logic/Geocoding/OpenStreetMapIdSearch" | ||||||
|  | import FilterSearch from "../Logic/Geocoding/FilterSearch" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * |  * | ||||||
|  | @ -385,9 +386,10 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
| 
 | 
 | ||||||
|         this.geosearch = new CombinedSearcher( |         this.geosearch = new CombinedSearcher( | ||||||
|             new CoordinateSearch(), |             new CoordinateSearch(), | ||||||
|             new LocalElementSearch(this, 5), |             new FilterSearch(this), | ||||||
|             new OpenStreetMapIdSearch(this), |             //new LocalElementSearch(this, 5),
 | ||||||
|             new PhotonSearch(), // new NominatimGeocoding(),
 |             //new OpenStreetMapIdSearch(this),
 | ||||||
|  |            // new PhotonSearch(), // new NominatimGeocoding(),
 | ||||||
|             this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this) : undefined |             this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this) : undefined | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
|       return |       return | ||||||
|     } |     } | ||||||
|     const v = value.data |     const v = value.data | ||||||
|     for (let option of htmlElement.getElementsByTagName("option")) { |     for (let option of Array.from(htmlElement.getElementsByTagName("option"))) { | ||||||
|       if (option.value === v) { |       if (option.value === v) { | ||||||
|         option.selected = true |         option.selected = true | ||||||
|         return |         return | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ | ||||||
|   import FilterviewWithFields from "./FilterviewWithFields.svelte" |   import FilterviewWithFields from "./FilterviewWithFields.svelte" | ||||||
|   import Tr from "../Base/Tr.svelte" |   import Tr from "../Base/Tr.svelte" | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|  |   import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
|   export let filteredLayer: FilteredLayer |   export let filteredLayer: FilteredLayer | ||||||
|   export let highlightedLayer: Store<string | undefined> = new ImmutableStore(undefined) |   export let highlightedLayer: Store<string | undefined> = new ImmutableStore(undefined) | ||||||
|  | @ -28,7 +29,7 @@ | ||||||
|     return state.sync( |     return state.sync( | ||||||
|       (f) => f === 0, |       (f) => f === 0, | ||||||
|       [], |       [], | ||||||
|       (b) => (b ? 0 : undefined) |       (b) => (b ? 0 : undefined), | ||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -75,6 +76,9 @@ | ||||||
|               <Dropdown value={getStateFor(filter)}> |               <Dropdown value={getStateFor(filter)}> | ||||||
|                 {#each filter.options as option, i} |                 {#each filter.options as option, i} | ||||||
|                   <option value={i}> |                   <option value={i}> | ||||||
|  |                     {#if Utils.isEmoji(option.icon)} | ||||||
|  |                       {option.icon} | ||||||
|  |                     {/if} | ||||||
|                     <Tr t={option.question} /> |                     <Tr t={option.question} /> | ||||||
|                   </option> |                   </option> | ||||||
|                 {/each} |                 {/each} | ||||||
|  |  | ||||||
|  | @ -1,121 +0,0 @@ | ||||||
| <script lang="ts"> |  | ||||||
|   import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider" |  | ||||||
|   import { GeocodingUtils } 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" |  | ||||||
|   import Icon from "../Map/Icon.svelte" |  | ||||||
|   import { BBox } from "../../Logic/BBox" |  | ||||||
|   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" |  | ||||||
|   import { UIEventSource } from "../../Logic/UIEventSource" |  | ||||||
|   import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp" |  | ||||||
|   import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
|   import Tr from "../Base/Tr.svelte" |  | ||||||
|   import { Translation } from "../i18n/Translation" |  | ||||||
|   import MoreScreen from "./MoreScreen" |  | ||||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" |  | ||||||
| 
 |  | ||||||
|   export let entry: GeoCodeResult |  | ||||||
|   export let state: SpecialVisualizationState |  | ||||||
|   let layer: LayerConfig |  | ||||||
|   let tags: UIEventSource<Record<string, string>> |  | ||||||
|   let descriptionTr: TagRenderingConfig = undefined |  | ||||||
|   if (entry.feature?.properties?.id) { |  | ||||||
|     layer = state.layout.getMatchingLayer(entry.feature.properties) |  | ||||||
|     tags = state.featureProperties.getStore(entry.feature.properties.id) |  | ||||||
|     descriptionTr = layer.tagRenderings.find(tr => tr.labels.indexOf("description") >= 0) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   let dispatch = createEventDispatcher<{ select }>() |  | ||||||
|   let distance = state.mapProperties.location.mapD(l => GeoOperations.distanceBetween([l.lon, l.lat], [entry.lon, entry.lat])) |  | ||||||
|   let bearing = state.mapProperties.location.mapD(l => GeoOperations.bearing([l.lon, l.lat], [entry.lon, entry.lat])) |  | ||||||
|   let mapRotation = state.mapProperties.rotation |  | ||||||
|   let inView = state.mapProperties.bounds.mapD(bounds => bounds.contains([entry.lon, entry.lat])) |  | ||||||
| 
 |  | ||||||
|   let otherTheme: MinimalLayoutInformation | undefined = <MinimalLayoutInformation>entry.payload |  | ||||||
| 
 |  | ||||||
|   function select() { |  | ||||||
|     if (entry.boundingbox) { |  | ||||||
|       const [lat0, lat1, lon0, lon1] = entry.boundingbox |  | ||||||
|       state.mapProperties.bounds.set( |  | ||||||
|         new BBox([ |  | ||||||
|           [lon0, lat0], |  | ||||||
|           [lon1, lat1] |  | ||||||
|         ]).pad(0.01) |  | ||||||
|       ) |  | ||||||
|     } else { |  | ||||||
|       state.mapProperties.flyTo(entry.lon, entry.lat, GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17) |  | ||||||
|     } |  | ||||||
|     if (entry.feature?.properties?.id) { |  | ||||||
|       state.selectedElement.set(entry.feature) |  | ||||||
|     } |  | ||||||
|     state.recentlySearched.addSelected(entry) |  | ||||||
|     dispatch("select") |  | ||||||
|   } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| {#if otherTheme} |  | ||||||
|   <a href={ MoreScreen.createUrlFor(otherTheme, false)} |  | ||||||
|      class="flex items-center p-2 w-full gap-y-2 rounded-xl searchresult"> |  | ||||||
| 
 |  | ||||||
|     <Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" /> |  | ||||||
|     <div class="flex flex-col"> |  | ||||||
|       <b> |  | ||||||
|         <Tr t={new Translation(otherTheme.title)} /> |  | ||||||
|       </b> |  | ||||||
|       <!--<Tr t={new Translation(otherTheme.shortDescription)} /> --> |  | ||||||
|     </div> |  | ||||||
|   </a> |  | ||||||
| 
 |  | ||||||
| {:else} |  | ||||||
|   <button class="unstyled w-full link-no-underline searchresult" 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")} /> |  | ||||||
|       {:else if entry.category} |  | ||||||
|         <Icon icon={GeocodingUtils.categoryToIcon[entry.category]} clss="w-6 h-6 shrink-0" color="#aaa" /> |  | ||||||
|       {/if} |  | ||||||
|       <div class="flex flex-col items-start pl-2 w-full"> |  | ||||||
|         <div class="flex flex-wrap gap-x-2 justify-between w-full"> |  | ||||||
|           <b class="nowrap"> |  | ||||||
|             {#if layer && $tags?.id} |  | ||||||
|               <TagRenderingAnswer config={layer.title} selectedElement={entry.feature} {state} {tags} {layer} /> |  | ||||||
|             {:else} |  | ||||||
|               {entry.display_name ?? entry.osm_id} |  | ||||||
|             {/if} |  | ||||||
|           </b> |  | ||||||
|           {#if $distance > 50} |  | ||||||
|             <div class="flex gap-x-1 items-center"> |  | ||||||
|               {#if $bearing && !$inView} |  | ||||||
|                 <ArrowUp class="w-4 h-4 shrink-0" style={`transform: rotate(${$bearing - $mapRotation}deg)`} /> |  | ||||||
|               {/if} |  | ||||||
|               {#if $distance} |  | ||||||
|                 {GeoOperations.distanceToHuman($distance)} |  | ||||||
|               {/if} |  | ||||||
|             </div> |  | ||||||
|           {/if} |  | ||||||
|         </div> |  | ||||||
|         <div class="flex flex-wrap gap-x-2"> |  | ||||||
| 
 |  | ||||||
|           {#if descriptionTr} |  | ||||||
|             <TagRenderingAnswer defaultSize="subtle" noIcons={true} config={descriptionTr} {tags} {state} |  | ||||||
|                                 selectedElement={entry.feature} {layer} /> |  | ||||||
|           {/if} |  | ||||||
|           {#if descriptionTr && entry.description} |  | ||||||
|             – |  | ||||||
|           {/if} |  | ||||||
|           {#if entry.description} |  | ||||||
|             <div class="subtle flex justify-between w-full"> |  | ||||||
|               {entry.description} |  | ||||||
|             </div> |  | ||||||
|           {/if} |  | ||||||
|         </div> |  | ||||||
| 
 |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </button> |  | ||||||
| 
 |  | ||||||
| {/if} |  | ||||||
|  | @ -1,100 +0,0 @@ | ||||||
| <script lang="ts"> |  | ||||||
|   import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider" |  | ||||||
|   import SearchResult from "./SearchResult.svelte" |  | ||||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" |  | ||||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" |  | ||||||
|   import Loading from "../Base/Loading.svelte" |  | ||||||
|   import Tr from "../Base/Tr.svelte" |  | ||||||
|   import Translations from "../i18n/Translations" |  | ||||||
|   import MoreScreen from "./MoreScreen" |  | ||||||
|   import { XCircleIcon } from "@babeard/svelte-heroicons/solid" |  | ||||||
| 
 |  | ||||||
|   export let state: SpecialVisualizationState |  | ||||||
|   export let results: GeoCodeResult[] |  | ||||||
|   export let searchTerm: Store<string> |  | ||||||
|   export let isFocused: UIEventSource<boolean> |  | ||||||
| 
 |  | ||||||
|   let recentlySeen: Store<GeoCodeResult[]> = state.recentlySearched.seenThisSession |  | ||||||
|   let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3)) |  | ||||||
|   let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <div class="relative w-full h-full collapsable " class:collapsed={!$isFocused}> |  | ||||||
|   <button class="absolute right-0 top-0 border-none p-0" on:click={() => isFocused.setData(false)} tabindex="-1"> |  | ||||||
|     <XCircleIcon class="w-6 h-6" /> |  | ||||||
|   </button> |  | ||||||
| 
 |  | ||||||
|   <div class="searchbox normal-background"> |  | ||||||
| 
 |  | ||||||
|     {#if $searchTerm.length > 0 && results === undefined} |  | ||||||
|       <div class="flex justify-center m-4 my-8"> |  | ||||||
|         <Loading /> |  | ||||||
|       </div> |  | ||||||
|     {:else if results?.length > 0} |  | ||||||
|       <div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2" tabindex="-1"> |  | ||||||
| 
 |  | ||||||
|         {#each results as entry (entry)} |  | ||||||
|           <SearchResult on:select {entry} {state} /> |  | ||||||
|         {/each} |  | ||||||
|       </div> |  | ||||||
|     {:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0} |  | ||||||
|       <div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2 flex flex-col gap-y-8" tabindex="-1"> |  | ||||||
|         {#if $searchTerm.length > 0} |  | ||||||
|           <b class="flex justify-center p-4"> |  | ||||||
|             <Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} /> |  | ||||||
|           </b> |  | ||||||
|         {/if} |  | ||||||
| 
 |  | ||||||
|         {#if $recentlySeen?.length > 0} |  | ||||||
|           <div> |  | ||||||
|             <h3 class="m-2"> |  | ||||||
|               <Tr t={Translations.t.general.search.recents} /> |  | ||||||
|             </h3> |  | ||||||
|             {#each $recentlySeen as entry} |  | ||||||
|               <SearchResult {entry} {state} on:select /> |  | ||||||
|             {/each} |  | ||||||
|           </div> |  | ||||||
|         {/if} |  | ||||||
| 
 |  | ||||||
|         {#if $recentThemes?.length > 0 && $allowOtherThemes} |  | ||||||
|           <div> |  | ||||||
|             <h3 class="m-2"> |  | ||||||
|               <Tr t={Translations.t.general.search.recentThemes} /> |  | ||||||
|             </h3> |  | ||||||
|             {#each $recentThemes as themeId (themeId)} |  | ||||||
|               <SearchResult |  | ||||||
|                 entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}} |  | ||||||
|                 {state} |  | ||||||
|                 on:select /> |  | ||||||
|             {/each} |  | ||||||
|           </div> |  | ||||||
|         {/if} |  | ||||||
|       </div> |  | ||||||
|     {/if} |  | ||||||
|   </div> |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
| <style> |  | ||||||
|     .searchbox { |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: column; |  | ||||||
|         row-gap: 0.5rem; |  | ||||||
|         padding: 0.5rem; |  | ||||||
|         border: 1px solid black; |  | ||||||
|         border-radius: 0.5rem; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .collapsable { |  | ||||||
|         max-height: 50vh; |  | ||||||
|         transition: max-height 400ms linear; |  | ||||||
|         transition-delay: 100ms; |  | ||||||
|         overflow: hidden; |  | ||||||
|         padding: 0 !important; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .collapsed { |  | ||||||
|         padding-top: 0 !important; |  | ||||||
|         padding-bottom: 0 !important; |  | ||||||
|         max-height: 0 !important; |  | ||||||
|     } |  | ||||||
| </style> |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|   import Tr from "../Base/Tr.svelte" |   import Tr from "../Base/Tr.svelte" | ||||||
|   import NextButton from "../Base/NextButton.svelte" |   import NextButton from "../Base/NextButton.svelte" | ||||||
|   import Geosearch from "./Geosearch.svelte" |   import Geosearch from "../Search/Geosearch.svelte" | ||||||
|   import ThemeViewState from "../../Models/ThemeViewState" |   import ThemeViewState from "../../Models/ThemeViewState" | ||||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" |   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
|   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" |   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ | ||||||
|   import { GeoOperations } from "../../Logic/GeoOperations" |   import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
|   import LocationInput from "../InputElement/Helpers/LocationInput.svelte" |   import LocationInput from "../InputElement/Helpers/LocationInput.svelte" | ||||||
|   import OpenBackgroundSelectorButton from "../BigComponents/OpenBackgroundSelectorButton.svelte" |   import OpenBackgroundSelectorButton from "../BigComponents/OpenBackgroundSelectorButton.svelte" | ||||||
|   import Geosearch from "../BigComponents/Geosearch.svelte" |   import Geosearch from "../Search/Geosearch.svelte" | ||||||
|   import If from "../Base/If.svelte" |   import If from "../Base/If.svelte" | ||||||
|   import Constants from "../../Models/Constants" |   import Constants from "../../Models/Constants" | ||||||
|   import LoginToggle from "../Base/LoginToggle.svelte" |   import LoginToggle from "../Base/LoginToggle.svelte" | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								src/UI/Search/ActiveFilter.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/UI/Search/ActiveFilter.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type { ActiveFilter } from "../../Logic/State/LayerState" | ||||||
|  |   import { Badge } from "flowbite-svelte" | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  |   import Tr from "../Base/Tr.svelte" | ||||||
|  | 
 | ||||||
|  |   export let state: SpecialVisualizationState | ||||||
|  | 
 | ||||||
|  |   export let activeFilter: ActiveFilter | ||||||
|  |   let { control, layer, filter } = activeFilter | ||||||
|  |   let option = control.map(c => { | ||||||
|  |     if (typeof c === "number") { | ||||||
|  |       return filter.options[c] | ||||||
|  |     } | ||||||
|  |     return filter.options[0] | ||||||
|  |   }) | ||||||
|  | </script> | ||||||
|  | <Badge dismissable large border rounded color="dark" on:close={() =>{  console.log( "dismiss"); return control.setData(undefined) }}> | ||||||
|  |   <Tr cls="whitespace-nowrap" t={$option.question} /> | ||||||
|  | </Badge> | ||||||
							
								
								
									
										17
									
								
								src/UI/Search/ActiveFilters.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/UI/Search/ActiveFilters.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  |   import { Badge } from "flowbite-svelte" | ||||||
|  |   import ActiveFilter from "./ActiveFilter.svelte" | ||||||
|  | 
 | ||||||
|  |   export let state: SpecialVisualizationState | ||||||
|  | 
 | ||||||
|  |   let activeFilters = state.layerState.activeFilters | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | <div class="flex flex-wrap gap-y-1 gap-x-1 button-unstyled"> | ||||||
|  | 
 | ||||||
|  |   {#each $activeFilters as activeFilter (activeFilter)} | ||||||
|  |     <ActiveFilter {activeFilter} {state} /> | ||||||
|  |   {/each} | ||||||
|  | </div> | ||||||
							
								
								
									
										55
									
								
								src/UI/Search/FilterResult.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/UI/Search/FilterResult.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type FilterConfig from "../../Models/ThemeConfig/FilterConfig" | ||||||
|  |   import type { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig" | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  |   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|  |   import Filter from "../../assets/svg/Filter.svelte" | ||||||
|  |   import Tr from "../Base/Tr.svelte" | ||||||
|  |   import type { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|  |   import { createEventDispatcher } from "svelte" | ||||||
|  |   import { FilterIcon as FilterSolid } from "@rgossiaux/svelte-heroicons/solid" | ||||||
|  |   import { FilterIcon as FilterOutline } from "@rgossiaux/svelte-heroicons/outline" | ||||||
|  | 
 | ||||||
|  |   export let entry: { | ||||||
|  |     category: "filter", | ||||||
|  |     payload: FilterPayload | ||||||
|  |   } | ||||||
|  |   let { option, filter, layer, index } = entry.payload | ||||||
|  |   export let state: SpecialVisualizationState | ||||||
|  |   let dispatch = createEventDispatcher<{ select }>() | ||||||
|  | 
 | ||||||
|  |   let flayer = state.layerState.filteredLayers.get(layer.id) | ||||||
|  |   let filtercontrol = flayer.appliedFilters.get(filter.id) | ||||||
|  |   let isActive = filtercontrol.map(c => c === index) | ||||||
|  | 
 | ||||||
|  |   function apply() { | ||||||
|  | 
 | ||||||
|  |     for (const [name, otherLayer] of state.layerState.filteredLayers) { | ||||||
|  |       if(name === layer.id){ | ||||||
|  |         otherLayer.isDisplayed.setData(true) | ||||||
|  |         continue | ||||||
|  |       } | ||||||
|  |       otherLayer.isDisplayed.setData(false) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(filtercontrol.data === index){ | ||||||
|  |       filtercontrol.setData(undefined) | ||||||
|  |     }else{ | ||||||
|  |       filtercontrol.setData(index) | ||||||
|  |     } | ||||||
|  |     dispatch("select") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  | <button on:click={() => apply()}> | ||||||
|  |   {#if $isActive} | ||||||
|  |     <FilterSolid class="w-8 h-8 shrink-0" /> | ||||||
|  |   {:else} | ||||||
|  |     <FilterOutline class="w-8 h-8 shrink-0" /> | ||||||
|  |   {/if} | ||||||
|  |   <Tr t={option.question} /> | ||||||
|  |   <div class="subtle"> | ||||||
|  |     {layer.id} | ||||||
|  |   </div> | ||||||
|  | </button> | ||||||
							
								
								
									
										100
									
								
								src/UI/Search/GeocodeResult.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/UI/Search/GeocodeResult.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,100 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|  |   import type { GeocodeResult } from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|  |   import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
|  |   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|  |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|  |   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||||
|  |   import { createEventDispatcher } from "svelte" | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  |   import { BBox } from "../../Logic/BBox" | ||||||
|  |   import ToSvelte from "../Base/ToSvelte.svelte" | ||||||
|  |   import Icon from "../Map/Icon.svelte" | ||||||
|  |   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" | ||||||
|  |   import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp" | ||||||
|  | 
 | ||||||
|  |   export let entry: GeocodeResult | ||||||
|  |   export let state: SpecialVisualizationState | ||||||
|  | 
 | ||||||
|  |   let layer: LayerConfig | ||||||
|  |   let tags: UIEventSource<Record<string, string>> | ||||||
|  |   let descriptionTr: TagRenderingConfig = undefined | ||||||
|  |   if (entry.feature?.properties?.id) { | ||||||
|  |     layer = state.layout.getMatchingLayer(entry.feature.properties) | ||||||
|  |     tags = state.featureProperties.getStore(entry.feature.properties.id) | ||||||
|  |     descriptionTr = layer.tagRenderings.find(tr => tr.labels.indexOf("description") >= 0) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let dispatch = createEventDispatcher<{ select }>() | ||||||
|  |   let distance = state.mapProperties.location.mapD(l => GeoOperations.distanceBetween([l.lon, l.lat], [entry.lon, entry.lat])) | ||||||
|  |   let bearing = state.mapProperties.location.mapD(l => GeoOperations.bearing([l.lon, l.lat], [entry.lon, entry.lat])) | ||||||
|  |   let mapRotation = state.mapProperties.rotation | ||||||
|  |   let inView = state.mapProperties.bounds.mapD(bounds => bounds.contains([entry.lon, entry.lat])) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   function select() { | ||||||
|  |     if (entry.boundingbox) { | ||||||
|  |       const [lat0, lat1, lon0, lon1] = entry.boundingbox | ||||||
|  |       state.mapProperties.bounds.set( | ||||||
|  |         new BBox([ | ||||||
|  |           [lon0, lat0], | ||||||
|  |           [lon1, lat1], | ||||||
|  |         ]).pad(0.01), | ||||||
|  |       ) | ||||||
|  |     } else { | ||||||
|  |       state.mapProperties.flyTo(entry.lon, entry.lat, GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17) | ||||||
|  |     } | ||||||
|  |     if (entry.feature?.properties?.id) { | ||||||
|  |       state.selectedElement.set(entry.feature) | ||||||
|  |     } | ||||||
|  |     state.recentlySearched.addSelected(entry) | ||||||
|  |     dispatch("select") | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <button class="unstyled w-full link-no-underline searchresult" 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")} /> | ||||||
|  |     {:else if entry.category} | ||||||
|  |       <Icon icon={GeocodingUtils.categoryToIcon[entry.category]} clss="w-6 h-6 shrink-0" color="#aaa" /> | ||||||
|  |     {/if} | ||||||
|  |     <div class="flex flex-col items-start pl-2 w-full"> | ||||||
|  |       <div class="flex flex-wrap gap-x-2 justify-between w-full"> | ||||||
|  |         <b class="nowrap"> | ||||||
|  |           {#if layer && $tags?.id} | ||||||
|  |             <TagRenderingAnswer config={layer.title} selectedElement={entry.feature} {state} {tags} {layer} /> | ||||||
|  |           {:else} | ||||||
|  |             {entry.display_name ?? entry.osm_id} | ||||||
|  |           {/if} | ||||||
|  |         </b> | ||||||
|  |         {#if $distance > 50} | ||||||
|  |           <div class="flex gap-x-1 items-center"> | ||||||
|  |             {#if $bearing && !$inView} | ||||||
|  |               <ArrowUp class="w-4 h-4 shrink-0" style={`transform: rotate(${$bearing - $mapRotation}deg)`} /> | ||||||
|  |             {/if} | ||||||
|  |             {#if $distance} | ||||||
|  |               {GeoOperations.distanceToHuman($distance)} | ||||||
|  |             {/if} | ||||||
|  |           </div> | ||||||
|  |         {/if} | ||||||
|  |       </div> | ||||||
|  |       <div class="flex flex-wrap gap-x-2"> | ||||||
|  | 
 | ||||||
|  |         {#if descriptionTr} | ||||||
|  |           <TagRenderingAnswer defaultSize="subtle" noIcons={true} config={descriptionTr} {tags} {state} | ||||||
|  |                               selectedElement={entry.feature} {layer} /> | ||||||
|  |         {/if} | ||||||
|  |         {#if descriptionTr && entry.description} | ||||||
|  |           – | ||||||
|  |         {/if} | ||||||
|  |         {#if entry.description} | ||||||
|  |           <div class="subtle flex justify-between w-full"> | ||||||
|  |             {entry.description} | ||||||
|  |           </div> | ||||||
|  |         {/if} | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </button> | ||||||
|  | @ -13,18 +13,16 @@ | ||||||
|   import { GeoLocationState } from "../../Logic/State/GeoLocationState" |   import { GeoLocationState } from "../../Logic/State/GeoLocationState" | ||||||
|   import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding" |   import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding" | ||||||
|   import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider" |   import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|   import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider" |   import type { SearchResult } from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|   import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider" |   import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider" | ||||||
| 
 | 
 | ||||||
|   import SearchResults from "./SearchResults.svelte" |   import SearchResults from "./SearchResults.svelte" | ||||||
|   import MoreScreen from "./MoreScreen" |  | ||||||
|   import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" |   import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||||
|   import { focusWithArrows } from "../../Utils/focusWithArrows" |   import { focusWithArrows } from "../../Utils/focusWithArrows" | ||||||
|   import ShowDataLayer from "../Map/ShowDataLayer" |   import ShowDataLayer from "../Map/ShowDataLayer" | ||||||
|   import ThemeViewState from "../../Models/ThemeViewState" |   import ThemeViewState from "../../Models/ThemeViewState" | ||||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" |  | ||||||
|   import GeocodingFeatureSource from "../../Logic/Geocoding/GeocodingFeatureSource" |   import GeocodingFeatureSource from "../../Logic/Geocoding/GeocodingFeatureSource" | ||||||
|   import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson.js" |   import MoreScreen from "../BigComponents/MoreScreen" | ||||||
| 
 | 
 | ||||||
|   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> | ||||||
|  | @ -90,8 +88,7 @@ | ||||||
|         return |         return | ||||||
|       } |       } | ||||||
|       const poi = result[0] |       const poi = result[0] | ||||||
|       if (poi.payload !== undefined) { |       if (poi.category === "theme") { | ||||||
|         // This is a theme |  | ||||||
|         const theme = <MinimalLayoutInformation>poi.payload |         const theme = <MinimalLayoutInformation>poi.payload | ||||||
|         const url = MoreScreen.createUrlFor(theme, false) |         const url = MoreScreen.createUrlFor(theme, false) | ||||||
|         console.log("Found a theme, going to", url) |         console.log("Found a theme, going to", url) | ||||||
|  | @ -99,6 +96,9 @@ | ||||||
|         window.location = url |         window.location = url | ||||||
|         return |         return | ||||||
|       } |       } | ||||||
|  |       if(poi.category === "filter"){ | ||||||
|  |         return  // Should not happen | ||||||
|  |       } | ||||||
|       if (poi.boundingbox) { |       if (poi.boundingbox) { | ||||||
| 
 | 
 | ||||||
|         const [lat0, lat1, lon0, lon1] = poi.boundingbox |         const [lat0, lat1, lon0, lon1] = poi.boundingbox | ||||||
|  | @ -139,13 +139,14 @@ | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   let suggestions: Store<GeoCodeResult[]> = searchContents.stabilized(250).bindD(search => { |   let suggestions: Store<SearchResult[]> = searchContents.stabilized(250).bindD(search => { | ||||||
|       if (search.length === 0) { |       if (search.length === 0) { | ||||||
|         return undefined |         return undefined | ||||||
|       } |       } | ||||||
|       return Stores.holdDefined(bounds.bindD(bbox => searcher.suggest(search, { bbox, limit: 15 }))) |       return Stores.holdDefined(bounds.bindD(bbox => searcher.suggest(search, { bbox, limit: 15 }))) | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
|  |   suggestions.addCallbackAndRun(suggestions => console.log(">>> suggestions are", suggestions)) | ||||||
|   let geocededFeatures=  new GeocodingFeatureSource(suggestions.stabilized(250)) |   let geocededFeatures=  new GeocodingFeatureSource(suggestions.stabilized(250)) | ||||||
|   state.featureProperties.trackFeatureSource(geocededFeatures) |   state.featureProperties.trackFeatureSource(geocededFeatures) | ||||||
| 
 | 
 | ||||||
							
								
								
									
										20
									
								
								src/UI/Search/SearchResult.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/UI/Search/SearchResult.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type { SearchResult } from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|  | 
 | ||||||
|  |   import ThemeResult from "../Search/ThemeResult.svelte" | ||||||
|  |   import FilterResult from "./FilterResult.svelte" | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  |   import GeocodeResult from "./GeocodeResult.svelte" | ||||||
|  | 
 | ||||||
|  |   export let entry: SearchResult | ||||||
|  |   export let state: SpecialVisualizationState | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if entry.category === "theme"} | ||||||
|  |   <ThemeResult {entry} /> | ||||||
|  | {:else if entry.category === "filter"} | ||||||
|  |   <FilterResult {entry} {state} /> | ||||||
|  | {:else} | ||||||
|  | 
 | ||||||
|  |   <GeocodeResult {entry} {state} /> | ||||||
|  | {/if} | ||||||
							
								
								
									
										102
									
								
								src/UI/Search/SearchResults.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/UI/Search/SearchResults.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,102 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  |   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
|  |   import Loading from "../Base/Loading.svelte" | ||||||
|  |   import Tr from "../Base/Tr.svelte" | ||||||
|  |   import Translations from "../i18n/Translations" | ||||||
|  |   import { default as SearchResultSvelte } from "./SearchResult.svelte" | ||||||
|  |   import MoreScreen from "../BigComponents/MoreScreen" | ||||||
|  |   import type { GeocodeResult, SearchResult } from "../../Logic/Geocoding/GeocodingProvider" | ||||||
|  |   import ActiveFilters from "./ActiveFilters.svelte" | ||||||
|  | 
 | ||||||
|  |   export let state: SpecialVisualizationState | ||||||
|  |   export let results: SearchResult[] | ||||||
|  |   export let searchTerm: Store<string> | ||||||
|  |   export let isFocused: UIEventSource<boolean> | ||||||
|  |   let hasActiveFilters = state.layerState.activeFilters.map(af => af.length > 0) | ||||||
|  | 
 | ||||||
|  |   console.log("Results are", results) | ||||||
|  | 
 | ||||||
|  |   let recentlySeen: Store<GeocodeResult[]> = state.recentlySearched.seenThisSession | ||||||
|  |   let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3)) | ||||||
|  |   let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="relative w-full h-full collapsable " class:collapsed={!$isFocused && !$hasActiveFilters}> | ||||||
|  |   <div class="searchbox normal-background"> | ||||||
|  |     <ActiveFilters {state} /> | ||||||
|  |     {#if $isFocused} | ||||||
|  |       {#if $searchTerm.length > 0 && results === undefined} | ||||||
|  |         <div class="flex justify-center m-4 my-8"> | ||||||
|  |           <Loading /> | ||||||
|  |         </div> | ||||||
|  |       {:else if results?.length > 0} | ||||||
|  |         <div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2" tabindex="-1"> | ||||||
|  | 
 | ||||||
|  |           {#each results as entry (entry)} | ||||||
|  |             <SearchResultSvelte on:select {entry} {state} /> | ||||||
|  |           {/each} | ||||||
|  |         </div> | ||||||
|  |       {:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0} | ||||||
|  |         <div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2 flex flex-col gap-y-8" | ||||||
|  |              tabindex="-1"> | ||||||
|  |           {#if $searchTerm.length > 0} | ||||||
|  |             <b class="flex justify-center p-4"> | ||||||
|  |               <Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} /> | ||||||
|  |             </b> | ||||||
|  |           {/if} | ||||||
|  | 
 | ||||||
|  |           {#if $recentlySeen?.length > 0} | ||||||
|  |             <div> | ||||||
|  |               <h3 class="m-2"> | ||||||
|  |                 <Tr t={Translations.t.general.search.recents} /> | ||||||
|  |               </h3> | ||||||
|  |               {#each $recentlySeen as entry} | ||||||
|  |                 <SearchResultSvelte {entry} {state} on:select /> | ||||||
|  |               {/each} | ||||||
|  |             </div> | ||||||
|  |           {/if} | ||||||
|  | 
 | ||||||
|  |           {#if $recentThemes?.length > 0 && $allowOtherThemes} | ||||||
|  |             <div> | ||||||
|  |               <h3 class="m-2"> | ||||||
|  |                 <Tr t={Translations.t.general.search.recentThemes} /> | ||||||
|  |               </h3> | ||||||
|  |               {#each $recentThemes as themeId (themeId)} | ||||||
|  |                 <SearchResultSvelte | ||||||
|  |                   entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}} | ||||||
|  |                   {state} | ||||||
|  |                   on:select /> | ||||||
|  |               {/each} | ||||||
|  |             </div> | ||||||
|  |           {/if} | ||||||
|  |         </div> | ||||||
|  |       {/if} | ||||||
|  |     {/if} | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     .searchbox { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         row-gap: 0.5rem; | ||||||
|  |         padding: 0.5rem; | ||||||
|  |         border: 1px solid black; | ||||||
|  |         border-radius: 0.5rem; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .collapsable { | ||||||
|  |         max-height: 50vh; | ||||||
|  |         transition: max-height 400ms linear; | ||||||
|  |         transition-delay: 100ms; | ||||||
|  |         overflow: hidden; | ||||||
|  |         padding: 0 !important; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .collapsed { | ||||||
|  |         padding-top: 0 !important; | ||||||
|  |         padding-bottom: 0 !important; | ||||||
|  |         max-height: 0 !important; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
							
								
								
									
										22
									
								
								src/UI/Search/ThemeResult.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/UI/Search/ThemeResult.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||||
|  |   import MoreScreen from "../BigComponents/MoreScreen" | ||||||
|  |   import { Translation } from "../i18n/Translation" | ||||||
|  |   import Icon from "../Map/Icon.svelte" | ||||||
|  |   import Tr from "../Base/Tr.svelte" | ||||||
|  | 
 | ||||||
|  |   export let entry:  { category: "theme", payload: MinimalLayoutInformation } | ||||||
|  |   let otherTheme = entry.payload | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <a href={MoreScreen.createUrlFor(otherTheme, false)} | ||||||
|  |    class="flex items-center p-2 w-full gap-y-2 rounded-xl searchresult"> | ||||||
|  | 
 | ||||||
|  |   <Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" /> | ||||||
|  |   <div class="flex flex-col"> | ||||||
|  |     <b> | ||||||
|  |       <Tr t={new Translation(otherTheme.title)} /> | ||||||
|  |     </b> | ||||||
|  |     <!--<Tr t={new Translation(otherTheme.shortDescription)} /> --> | ||||||
|  |   </div> | ||||||
|  | </a> | ||||||
|  | @ -11,7 +11,7 @@ | ||||||
|   import LayerConfig from "../Models/ThemeConfig/LayerConfig" |   import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||||
|   import ThemeViewState from "../Models/ThemeViewState" |   import ThemeViewState from "../Models/ThemeViewState" | ||||||
|   import type { MapProperties } from "../Models/MapProperties" |   import type { MapProperties } from "../Models/MapProperties" | ||||||
|   import Geosearch from "./BigComponents/Geosearch.svelte" |   import Geosearch from "./Search/Geosearch.svelte" | ||||||
|   import Translations from "./i18n/Translations" |   import Translations from "./i18n/Translations" | ||||||
|   import usersettings from "../assets/generated/layers/usersettings.json" |   import usersettings from "../assets/generated/layers/usersettings.json" | ||||||
|   import { |   import { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import DOMPurify from "dompurify" | import DOMPurify from "dompurify" | ||||||
| 
 |  | ||||||
| export class Utils { | export class Utils { | ||||||
|     /** |     /** | ||||||
|      * In the 'deploy'-step, some code needs to be run by ts-node. |      * In the 'deploy'-step, some code needs to be run by ts-node. | ||||||
|  |  | ||||||
|  | @ -279,12 +279,12 @@ button.as-link { | ||||||
|     padding: 0; |     padding: 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| button.unstyled { | button.unstyled, .button-unstyled button { | ||||||
|     background-color: unset; |     background-color: unset; | ||||||
|     display: inline-flex; |     display: inline-flex; | ||||||
|     justify-content: start; |     justify-content: start; | ||||||
|     border: none; |     border: none; | ||||||
|     box-shadow: none; |     box-shadow: none !important; | ||||||
|     margin: 0; |     margin: 0; | ||||||
|     padding: 0; |     padding: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue