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", | ||||
|           "icon": "./assets/layers/questions/no_dogs.svg", | ||||
|           "icon": "\uD83D\uDC15 ⃠", | ||||
|           "then": { | ||||
|             "en": "Dogs are <b>not</b> allowed", | ||||
|             "nl": "honden zijn <b>niet</b> toegelaten", | ||||
|  |  | |||
|  | @ -257,6 +257,7 @@ | |||
|     { | ||||
|       "builtin": "id_presets.shop_types", | ||||
|       "override": { | ||||
|         "id": "shop_types", | ||||
|         "labels": [ | ||||
|           "description" | ||||
|         ], | ||||
|  | @ -1174,51 +1175,7 @@ | |||
|   ], | ||||
|   "filter": [ | ||||
|     "open_now", | ||||
|     { | ||||
|       "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}" | ||||
|           } | ||||
|         } | ||||
|       ] | ||||
|     }, | ||||
|     "shop_types", | ||||
|     { | ||||
|       "id": "second_hand", | ||||
|       "options": [ | ||||
|  |  | |||
|  | @ -260,6 +260,7 @@ | |||
|         "examples": "Examples", | ||||
|         "fewChangesBefore": "Please, answer a few questions of existing features before adding a new feature.", | ||||
|         "filterPanel": { | ||||
|             "allTypes": "All types", | ||||
|             "disableAll": "Disable all", | ||||
|             "enableAll": "Enable all" | ||||
|         }, | ||||
|  |  | |||
|  | @ -6700,15 +6700,6 @@ | |||
|             } | ||||
|         }, | ||||
|         "description": "Una botiga", | ||||
|         "filter": { | ||||
|             "1": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Sols mostrar botigues que venen {search}" | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "name": "Botiga", | ||||
|         "presets": { | ||||
|             "0": { | ||||
|  |  | |||
|  | @ -6979,21 +6979,7 @@ | |||
|         }, | ||||
|         "description": "Obchod", | ||||
|         "filter": { | ||||
|             "1": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Zobrazit pouze obchody prodávající {search}" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "2": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Zobrazit pouze obchody s názvem {search}" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "3": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Zobrazit pouze obchody prodávající použité zboží" | ||||
|  |  | |||
|  | @ -1419,6 +1419,13 @@ | |||
|                                 "arialabel": "Åbn på openstreetmap.org" | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     "2": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "arialabel": "Åbn på openstreetmap.org" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 "render": { | ||||
|  |  | |||
|  | @ -5673,6 +5673,13 @@ | |||
|                                 "arialabel": "Auf openstreetmap.org öffnen" | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     "2": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "arialabel": "Auf openstreetmap.org öffnen" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 "render": { | ||||
|  | @ -8855,21 +8862,7 @@ | |||
|         }, | ||||
|         "description": "Ein Geschäft", | ||||
|         "filter": { | ||||
|             "1": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Nur Geschäfte, die {search} verkaufen" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "2": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Nur Geschäfte mit dem Namen {search} anzeigen" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "3": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Nur Second-Hand-Geschäfte anzeigen" | ||||
|  |  | |||
|  | @ -5673,6 +5673,13 @@ | |||
|                                 "arialabel": "Open on openstreetmap.org" | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     "2": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "arialabel": "Open on openstreetmap.org" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 "render": { | ||||
|  | @ -8809,6 +8816,14 @@ | |||
|             "render": "School <i>{name}</i>" | ||||
|         } | ||||
|     }, | ||||
|     "search": { | ||||
|         "description": "Priviliged layer showing the search results", | ||||
|         "tagRenderings": { | ||||
|             "intro": { | ||||
|                 "render": "Search result" | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "selected_element": { | ||||
|         "description": "Highlights the currently selected element. Override this layer to have different colors" | ||||
|     }, | ||||
|  | @ -8855,21 +8870,7 @@ | |||
|         }, | ||||
|         "description": "A shop", | ||||
|         "filter": { | ||||
|             "1": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Only show shops selling {search}" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "2": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Only show shops with name {search}" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "3": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Only show shops selling second-hand items" | ||||
|  |  | |||
|  | @ -3796,22 +3796,6 @@ | |||
|             } | ||||
|         }, | ||||
|         "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", | ||||
|         "presets": { | ||||
|             "0": { | ||||
|  |  | |||
|  | @ -5618,22 +5618,6 @@ | |||
|             } | ||||
|         }, | ||||
|         "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", | ||||
|         "presets": { | ||||
|             "0": { | ||||
|  |  | |||
|  | @ -4664,6 +4664,13 @@ | |||
|                                 "arialabel": "Bekijk op openstreetmap.org" | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     "2": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "arialabel": "Bekijk op openstreetmap.org" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 "render": { | ||||
|  | @ -7120,22 +7127,6 @@ | |||
|             } | ||||
|         }, | ||||
|         "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", | ||||
|         "presets": { | ||||
|             "0": { | ||||
|  |  | |||
|  | @ -1948,6 +1948,13 @@ | |||
|                                 "arialabel": "Otwórz na openstreetmap.org" | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     "2": { | ||||
|                         "then": { | ||||
|                             "special": { | ||||
|                                 "arialabel": "Otwórz na openstreetmap.org" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 "render": { | ||||
|  |  | |||
|  | @ -953,6 +953,11 @@ video { | |||
|   margin-right: 1rem; | ||||
| } | ||||
| 
 | ||||
| .my-8 { | ||||
|   margin-top: 2rem; | ||||
|   margin-bottom: 2rem; | ||||
| } | ||||
| 
 | ||||
| .-my-1\.5 { | ||||
|   margin-top: -0.375rem; | ||||
|   margin-bottom: -0.375rem; | ||||
|  | @ -978,11 +983,6 @@ video { | |||
|   margin-right: -0.25rem; | ||||
| } | ||||
| 
 | ||||
| .my-8 { | ||||
|   margin-top: 2rem; | ||||
|   margin-bottom: 2rem; | ||||
| } | ||||
| 
 | ||||
| .mx-12 { | ||||
|   margin-left: 3rem; | ||||
|   margin-right: 3rem; | ||||
|  | @ -993,10 +993,6 @@ video { | |||
|   margin-right: 4rem; | ||||
| } | ||||
| 
 | ||||
| .mb-4 { | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
| 
 | ||||
| .mt-4 { | ||||
|   margin-top: 1rem; | ||||
| } | ||||
|  | @ -1029,6 +1025,10 @@ video { | |||
|   margin-right: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .mb-4 { | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
| 
 | ||||
| .ml-1 { | ||||
|   margin-left: 0.25rem; | ||||
| } | ||||
|  | @ -1440,10 +1440,6 @@ video { | |||
|   max-height: 3rem; | ||||
| } | ||||
| 
 | ||||
| .max-h-96 { | ||||
|   max-height: 24rem; | ||||
| } | ||||
| 
 | ||||
| .max-h-24 { | ||||
|   max-height: 6rem; | ||||
| } | ||||
|  | @ -1452,6 +1448,10 @@ video { | |||
|   max-height: 16rem; | ||||
| } | ||||
| 
 | ||||
| .max-h-96 { | ||||
|   max-height: 24rem; | ||||
| } | ||||
| 
 | ||||
| .max-h-full { | ||||
|   max-height: 100%; | ||||
| } | ||||
|  | @ -4623,12 +4623,12 @@ button.as-link { | |||
|   padding: 0; | ||||
| } | ||||
| 
 | ||||
| button.unstyled { | ||||
| button.unstyled, .button-unstyled button { | ||||
|   background-color: unset; | ||||
|   display: inline-flex; | ||||
|   justify-content: start; | ||||
|   border: none; | ||||
|   box-shadow: none; | ||||
|   box-shadow: none !important; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
| } | ||||
|  | @ -7234,14 +7234,14 @@ svg.apply-fill path { | |||
|     width: 50%; | ||||
|   } | ||||
| 
 | ||||
|   .sm\:w-96 { | ||||
|     width: 24rem; | ||||
|   } | ||||
| 
 | ||||
|   .sm\:w-11 { | ||||
|     width: 2.75rem; | ||||
|   } | ||||
| 
 | ||||
|   .sm\:w-96 { | ||||
|     width: 24rem; | ||||
|   } | ||||
| 
 | ||||
|   .sm\:w-auto { | ||||
|     width: auto; | ||||
|   } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||
| import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" | ||||
| import { Utils } from "../../Utils" | ||||
| import { Store, Stores } from "../UIEventSource" | ||||
| 
 | ||||
|  | @ -17,12 +17,17 @@ export default class CombinedSearcher implements GeocodingProvider { | |||
|      * @param geocoded | ||||
|      * @private | ||||
|      */ | ||||
|     private merge(geocoded: GeoCodeResult[][]): GeoCodeResult[] { | ||||
|         const results: GeoCodeResult[] = [] | ||||
|     private merge(geocoded: SearchResult[][]): SearchResult[] { | ||||
|         const results: SearchResult[] = [] | ||||
|         const seenIds = new Set<string>() | ||||
|         for (const geocodedElement of geocoded) { | ||||
|             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)) { | ||||
|                     continue | ||||
|                 } | ||||
|  | @ -33,13 +38,14 @@ export default class CombinedSearcher implements GeocodingProvider { | |||
|         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)))) | ||||
|         return results.flatMap(x => x) | ||||
|         return this.merge(results) | ||||
|     } | ||||
| 
 | ||||
|     suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> { | ||||
|         return Stores.concat(this._providersWithSuggest.map(pr => pr.suggest(query, options))) | ||||
|     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||
|         return Stores.concat( | ||||
|             this._providersWithSuggest.map(pr => pr.suggest(query, options))) | ||||
|             .map(gcrss => this.merge(gcrss)) | ||||
| 
 | ||||
|     } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider" | ||||
| import GeocodingProvider, { SearchResult } from "./GeocodingProvider" | ||||
| import { Utils } from "../../Utils" | ||||
| import { ImmutableStore, Store } from "../UIEventSource" | ||||
| 
 | ||||
|  | @ -52,9 +52,9 @@ export default class CoordinateSearch implements GeocodingProvider { | |||
|      * results.length // => 1
 | ||||
|      * 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]), | ||||
|             lon: Number(m[2]), | ||||
|             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))) | ||||
|             .map(m => <GeoCodeResult>{ | ||||
|             .map(m => <SearchResult>{ | ||||
|                 lat: Number(m[2]), | ||||
|                 lon: Number(m[1]), | ||||
|                 display_name: "lon: " + m[1] + ", lat: " + m[2], | ||||
|  | @ -74,11 +74,11 @@ export default class CoordinateSearch implements GeocodingProvider { | |||
|         return matches.concat(matchesLonLat) | ||||
|     } | ||||
| 
 | ||||
|     suggest(query: string): Store<GeoCodeResult[]> { | ||||
|     suggest(query: string): Store<SearchResult[]> { | ||||
|         return new ImmutableStore(this.directSearch(query)) | ||||
|     } | ||||
| 
 | ||||
|     async search (query: string): Promise<GeoCodeResult[]> { | ||||
|     async search (query: string): Promise<SearchResult[]> { | ||||
|         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 { FeatureSource } from "../FeatureSource/FeatureSource" | ||||
| import { Feature, Geometry } from "geojson" | ||||
|  | @ -6,7 +6,7 @@ import { Feature, Geometry } from "geojson" | |||
| export default class GeocodingFeatureSource implements FeatureSource { | ||||
|     public features: Store<Feature<Geometry, Record<string, string>>[]> | ||||
| 
 | ||||
|     constructor(provider: Store<GeoCodeResult[]>) { | ||||
|     constructor(provider: Store<SearchResult[]>) { | ||||
|         this.features = provider.map(geocoded => { | ||||
|             if(geocoded === undefined){ | ||||
|                 return [] | ||||
|  |  | |||
|  | @ -5,9 +5,22 @@ import { Store } from "../UIEventSource" | |||
| import * as search from "../../assets/generated/layers/search.json" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| 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 | ||||
|      */ | ||||
|  | @ -25,11 +38,16 @@ export type GeoCodeResult = { | |||
|      */ | ||||
|     boundingbox?: number[] | ||||
|     osm_type?: "node" | "way" | "relation" | ||||
|     osm_id?: string, | ||||
|     osm_id: string, | ||||
|     category?: GeocodingCategory, | ||||
|     payload?: object, | ||||
|     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 { | ||||
|     bbox?: BBox, | ||||
|  | @ -40,16 +58,16 @@ export interface GeocodingOptions { | |||
| export default interface GeocodingProvider { | ||||
| 
 | ||||
| 
 | ||||
|     search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> | ||||
|     search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> | ||||
| 
 | ||||
|     /** | ||||
|      * @param query | ||||
|      * @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_type: "node" | "way" | "relation", | ||||
|     country: string, | ||||
|  | @ -57,25 +75,26 @@ export type ReverseGeocodingResult = Feature<Geometry,{ | |||
|     countrycode: string, | ||||
|     type: GeocodingCategory, | ||||
|     street: string | ||||
| } > | ||||
| }> | ||||
| 
 | ||||
| export interface ReverseGeocodingProvider { | ||||
|     reverseSearch( | ||||
|         coordinate: { lon: number; lat: number }, | ||||
|         zoom: number, | ||||
|         language?: string | ||||
|     ): Promise<ReverseGeocodingResult[]> ; | ||||
|         language?: string, | ||||
|     ): Promise<ReverseGeocodingResult[]>; | ||||
| } | ||||
| 
 | ||||
| export class GeocodingUtils { | ||||
| 
 | ||||
|     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
 | ||||
|             return undefined | ||||
|         } | ||||
|         return new LayerConfig(<LayerConfigJson> search, "search") | ||||
|         return new LayerConfig(<LayerConfigJson>search, "search") | ||||
|     } | ||||
| 
 | ||||
|     public static categoryToZoomLevel: Record<GeocodingCategory, number> = { | ||||
|  | @ -88,7 +107,7 @@ export class GeocodingUtils { | |||
|         street: 15, | ||||
|         train_station: 14, | ||||
|         airport: 13, | ||||
|         shop:16 | ||||
|         shop: 16, | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  | @ -103,7 +122,7 @@ export class GeocodingUtils { | |||
|         train_station: "train", | ||||
|         county: "building_office_2", | ||||
|         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 { Utils } from "../../Utils" | ||||
| 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 | ||||
|     } | ||||
| 
 | ||||
|  | @ -41,7 +41,6 @@ export default class LocalElementSearch implements GeocodingProvider { | |||
|                     props["addr:street"] + props["addr:number"] : undefined]) | ||||
| 
 | ||||
|             let levehnsteinD: number | ||||
|             console.log("Comparing nearby:", candidateId, props.id) | ||||
|             if (candidateId === props.id) { | ||||
|                 levehnsteinD = 0 | ||||
|             } else { | ||||
|  | @ -54,7 +53,7 @@ export default class LocalElementSearch implements GeocodingProvider { | |||
|                 })) | ||||
|             } | ||||
|             const center = GeoOperations.centerpointCoordinates(feature) | ||||
|             if (levehnsteinD <= 2) { | ||||
|             if ((levehnsteinD / query.length) <= 0.3) { | ||||
| 
 | ||||
|                 let description = "" | ||||
|                 if (feature.properties["addr:street"]) { | ||||
|  | @ -76,7 +75,7 @@ export default class LocalElementSearch implements GeocodingProvider { | |||
|         return results | ||||
|     } | ||||
| 
 | ||||
|     searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<GeoCodeResult[]> { | ||||
|     searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<SearchResult[]> { | ||||
|         if (query.length < 3) { | ||||
|             return new ImmutableStore([]) | ||||
|         } | ||||
|  | @ -101,7 +100,7 @@ export default class LocalElementSearch implements GeocodingProvider { | |||
|             } | ||||
|             return results.map(entry => { | ||||
|                 const [osm_type, osm_id] = entry.feature.properties.id.split("/") | ||||
|                 return <GeoCodeResult>{ | ||||
|                 return <SearchResult>{ | ||||
|                     lon: entry.center[0], | ||||
|                     lat: entry.center[1], | ||||
|                     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) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import { BBox } from "../BBox" | |||
| import Constants from "../../Models/Constants" | ||||
| import { FeatureCollection } from "geojson" | ||||
| import Locale from "../../UI/i18n/Locale" | ||||
| import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider" | ||||
| import GeocodingProvider, { SearchResult } from "./GeocodingProvider" | ||||
| 
 | ||||
| export class NominatimGeocoding implements GeocodingProvider { | ||||
| 
 | ||||
|  | @ -13,7 +13,7 @@ export class NominatimGeocoding implements GeocodingProvider { | |||
|         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 url = `${ | ||||
|             this._host | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||
| import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" | ||||
| import { OsmId } from "../../Models/OsmFeature" | ||||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||
| 
 | ||||
|  | @ -30,7 +30,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider { | |||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|     async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||
|     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { | ||||
|         const id = OpenStreetMapIdSearch.extractId(query) | ||||
|         if (!id) { | ||||
|             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)) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| import Constants from "../../Models/Constants" | ||||
| import GeocodingProvider, { | ||||
|     GeoCodeResult, | ||||
|     GeocodeResult, | ||||
|     GeocodingCategory, | ||||
|     GeocodingOptions, | ||||
|     ReverseGeocodingProvider, | ||||
|     ReverseGeocodingResult | ||||
|     ReverseGeocodingResult, | ||||
| } from "./GeocodingProvider" | ||||
| import { Utils } from "../../Utils" | ||||
| import { Feature, FeatureCollection } from "geojson" | ||||
|  | @ -18,7 +18,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | |||
|     private static readonly types = { | ||||
|         "R": "relation", | ||||
|         "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)) | ||||
|     } | ||||
| 
 | ||||
|  | @ -95,7 +95,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | |||
| 
 | ||||
|     private getCategory(entry: Feature) { | ||||
|         const p = entry.properties | ||||
|         if(p.osm_key === "shop"){ | ||||
|         if (p.osm_key === "shop") { | ||||
|             return "shop" | ||||
|         } | ||||
|         if (p.osm_value === "train_station" || p.osm_key === "railway") { | ||||
|  | @ -107,7 +107,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | |||
|         return p.type | ||||
|     } | ||||
| 
 | ||||
|     async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||
|     async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> { | ||||
|         if (query.length < 3) { | ||||
|             return [] | ||||
|         } | ||||
|  | @ -126,7 +126,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | |||
|                 const [lon0, lat0, lon1, lat1] = f.properties.extent | ||||
|                 boundingbox = [lat0, lat1, lon0, lon1] | ||||
|             } | ||||
|             return <GeoCodeResult>{ | ||||
|             return <GeocodeResult>{ | ||||
|                 feature: f, | ||||
|                 osm_id: f.properties.osm_id, | ||||
|                 display_name: f.properties.name, | ||||
|  | @ -135,7 +135,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding | |||
|                 category: this.getCategory(f), | ||||
|                 boundingbox, | ||||
|                 lon, lat, | ||||
|                 source: this._endpoint | ||||
|                 source: this._endpoint, | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  |  | |||
|  | @ -1,36 +1,41 @@ | |||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import { Feature } from "geojson" | ||||
| import { OsmConnection } from "../Osm/OsmConnection" | ||||
| import { GeoCodeResult } from "./GeocodingProvider" | ||||
| import { GeocodeResult } from "./GeocodingProvider" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| 
 | ||||
| export class RecentSearch { | ||||
| 
 | ||||
| 
 | ||||
|     private readonly _seenThisSession: UIEventSource<GeoCodeResult[]> | ||||
|     public readonly seenThisSession: Store<GeoCodeResult[]> | ||||
|     private readonly _seenThisSession: UIEventSource<GeocodeResult[]> | ||||
|     public readonly seenThisSession: Store<GeocodeResult[]> | ||||
| 
 | ||||
|     constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) { | ||||
|         const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches") | ||||
|         prefs.addCallbackAndRunD(prev => console.trace("Previous searches are:", prev)) | ||||
|         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 | ||||
| 
 | ||||
|         prefs.addCallbackAndRunD(prefs => { | ||||
|             if(prefs === ""){ | ||||
|         prefs.addCallbackAndRunD(pref => { | ||||
|             if (pref === "") { | ||||
|                 return | ||||
|             } | ||||
|             const simpleArr = <GeoCodeResult[]> JSON.parse(prefs) | ||||
|             if(simpleArr.length > 0){ | ||||
|             try { | ||||
| 
 | ||||
|                 const simpleArr = <GeocodeResult[]>JSON.parse(pref) | ||||
|                 if (simpleArr.length > 0) { | ||||
|                     this._seenThisSession.set(simpleArr) | ||||
|                     return true | ||||
|                 } | ||||
|             } catch (e) { | ||||
|                 console.error(e, pref) | ||||
|                 prefs.setData("") | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         this.seenThisSession.stabilized(2500).addCallbackAndRunD(seen => { | ||||
|             const results=  [] | ||||
|             const results = [] | ||||
|             for (let i = 0; i < Math.min(3, seen.length); i++) { | ||||
|                 const gc = seen[i] | ||||
|                 const simple = { | ||||
|  | @ -39,7 +44,7 @@ export class RecentSearch { | |||
|                     display_name: gc.display_name, | ||||
|                     lat: gc.lat, lon: gc.lon, | ||||
|                     osm_id: gc.osm_id, | ||||
|                     osm_type: gc.osm_type | ||||
|                     osm_type: gc.osm_type, | ||||
|                 } | ||||
|                 results.push(simple) | ||||
|             } | ||||
|  | @ -51,25 +56,25 @@ export class RecentSearch { | |||
|         state.selectedElement.addCallbackAndRunD(selected => { | ||||
| 
 | ||||
|             const [osm_type, osm_id] = selected.properties.id.split("/") | ||||
|             if(!osm_id){ | ||||
|             if (!osm_id) { | ||||
|                 return | ||||
|             } | ||||
|             console.log("Selected element is", selected) | ||||
|             if(["node","way","relation"].indexOf(osm_type) < 0){ | ||||
|             if (["node", "way", "relation"].indexOf(osm_type) < 0) { | ||||
|                 return | ||||
|             } | ||||
|             const [lon, lat] = GeoOperations.centerpointCoordinates(selected) | ||||
|             const entry = <GeoCodeResult>{ | ||||
|             const entry = <GeocodeResult>{ | ||||
|                 feature: selected, | ||||
|                 osm_id, osm_type, | ||||
|                 lon, lat | ||||
|                 lon, lat, | ||||
|             } | ||||
|             this.addSelected(entry) | ||||
| 
 | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     addSelected(entry: GeoCodeResult) { | ||||
|     addSelected(entry: GeocodeResult) { | ||||
|         const arr = [...(this.seenThisSession.data ?? []).slice(0, 20), entry] | ||||
| 
 | ||||
|         const seenIds = new Set<string>() | ||||
|  | @ -81,7 +86,7 @@ export class RecentSearch { | |||
|                 seenIds.add(id) | ||||
|             } | ||||
|         } | ||||
|         console.log(">>>",arr) | ||||
|         console.log(">>>", 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 { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||
|  | @ -17,15 +17,15 @@ export default class ThemeSearch implements GeocodingProvider { | |||
|         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) | ||||
|     } | ||||
| 
 | ||||
|     suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> { | ||||
|     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||
|         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){ | ||||
|             return [] | ||||
|         } | ||||
|  | @ -37,8 +37,9 @@ export default class ThemeSearch implements GeocodingProvider { | |||
|             .filter(th => MoreScreen.MatchesLayout(th, query)) | ||||
|             .slice(0, limit + 1) | ||||
| 
 | ||||
|         return withMatch.map(match => <GeoCodeResult> { | ||||
|         return withMatch.map(match => <SearchResult> { | ||||
|             payload: match, | ||||
|             category: "theme", | ||||
|             osm_id: match.id | ||||
|         }) | ||||
|     } | ||||
|  |  | |||
|  | @ -7,7 +7,13 @@ import { Tag } from "../Tags/Tag" | |||
| import Translations from "../../UI/i18n/Translations" | ||||
| import { RegexTag } from "../Tags/RegexTag" | ||||
| 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: | ||||
|  * - 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 | ||||
|      */ | ||||
|     public readonly filteredLayers: ReadonlyMap<string, FilteredLayer> | ||||
|     private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([]) | ||||
| 
 | ||||
|     public readonly activeFilters: Store<ActiveFilter[]> = this._activeFilters | ||||
|     private readonly osmConnection: OsmConnection | ||||
| 
 | ||||
|     /** | ||||
|  | @ -56,6 +65,41 @@ export default class LayerState { | |||
|         } | ||||
|         this.filteredLayers = 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 | ||||
|     } | ||||
|         | undefined { | ||||
|         console.log("GETTING UNOFFICIAL THEME") | ||||
|         const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) | ||||
|         const str = pref.data | ||||
| 
 | ||||
|  | @ -351,10 +350,8 @@ export default class UserRelatedState { | |||
|                     const key = k.substring(0, k.length - "length".length) | ||||
|                     let combined = "" | ||||
|                     for (let i = 0; i < l; i++) { | ||||
|                         console.log("Building preference:",key,i,">>>", newPrefs[key + i], "<<<", newPrefs, ) | ||||
|                         combined += (newPrefs[key + i]) | ||||
|                     } | ||||
|                     console.log("Combined",key,">>>",combined) | ||||
|                     amendedPrefs.data[key.substring(0, key.length - "-combined-".length)] = combined | ||||
|                 } else { | ||||
|                     amendedPrefs.data[k] = newPrefs[k] | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ import LineRenderingConfigJson from "../Json/LineRenderingConfigJson" | |||
| import { ConversionContext } from "./ConversionContext" | ||||
| import { ExpandRewrite } from "./ExpandRewrite" | ||||
| import { TagUtils } from "../../../Logic/Tags/TagUtils" | ||||
| import { Translatable } from "../Json/Translatable" | ||||
| 
 | ||||
| class ExpandFilter extends DesugaringStep<LayerConfigJson> { | ||||
|     private static readonly predefinedFilters = ExpandFilter.load_filters() | ||||
|  | @ -40,7 +41,7 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> { | |||
| 
 | ||||
|     constructor(state: DesugaringContext) { | ||||
|         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"], | ||||
|             "ExpandFilter", | ||||
|         ) | ||||
|  | @ -67,6 +68,9 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> { | |||
|         const newFilters: FilterConfigJson[] = [] | ||||
|         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++) { | ||||
|             const tagRendering = <TagRenderingConfigJson>json.tagRenderings[i] | ||||
|             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++) { | ||||
|             const filter = filters[i] | ||||
|             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", | ||||
|                         ) | ||||
|                 } | ||||
|                 const options = matchingTr.mappings.map((mapping) => ({ | ||||
|                 const options = (<QuestionableTagRenderingConfigJson> matchingTr).mappings.map((mapping) => ({ | ||||
|                     question: mapping.then, | ||||
|                     osmTags: mapping.if, | ||||
|                     searchTerms: mapping.searchTerms | ||||
| 
 | ||||
|                 })) | ||||
|                 options.unshift({ | ||||
|                     question: matchingTr["question"] ?? { | ||||
|                         en: "All types", | ||||
|                     }, | ||||
|                     question: matchingTr["question"] ?? Translations.t.general.filterPanel.allTypes, | ||||
|                     osmTags: undefined, | ||||
|                     searchTerms: undefined | ||||
|                 }) | ||||
|                 newFilters.push({ | ||||
|                     id: filter, | ||||
|  |  | |||
|  | @ -103,6 +103,10 @@ export class DoesImageExist extends DesugaringStep<string> { | |||
|             return image | ||||
|         } | ||||
| 
 | ||||
|         if(Utils.isEmoji(image)){ | ||||
|             return image | ||||
|         } | ||||
| 
 | ||||
|         if (!this._knownImagePaths.has(image)) { | ||||
|             if (this.doesPathExist === undefined) { | ||||
|                 context.err( | ||||
|  |  | |||
|  | @ -8,12 +8,12 @@ import { UIEventSource } from "../../Logic/UIEventSource" | |||
| import { QueryParameters } from "../../Logic/Web/QueryParameters" | ||||
| import { Utils } from "../../Utils" | ||||
| 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" | ||||
| 
 | ||||
| export type FilterConfigOption = { | ||||
|     question: Translation | ||||
|     searchTerms: Record<string, string[]> | ||||
|     icon?: string | ||||
|     osmTags: TagsFilter | undefined | ||||
|     /* Only set if fields are present. Used to create `osmTags` (which are used to _actually_ filter) when the field is written*/ | ||||
|     readonly originalTagsSpec: TagConfigJson | ||||
|  | @ -105,8 +105,10 @@ export default class FilterConfig { | |||
|             return { | ||||
|                 question: question, | ||||
|                 osmTags: osmTags, | ||||
|                 searchTerms: option.searchTerms, | ||||
|                 fields, | ||||
|                 originalTagsSpec: option.osmTags, | ||||
|                 icon: option.icon | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|  | @ -151,7 +153,7 @@ export default class FilterConfig { | |||
|     } | ||||
| 
 | ||||
|     public initState(layerId: string): UIEventSource<undefined | number | string> { | ||||
|         let defaultValue = "" | ||||
|         let defaultValue: string | ||||
|         if (this.options.length > 1) { | ||||
|             defaultValue = "" + (this.defaultSelection ?? 0) | ||||
|         } else if (this.options[0].fields?.length > 0) { | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| import { TagConfigJson } from "./TagConfigJson" | ||||
| import { Translatable } from "./Translatable" | ||||
| 
 | ||||
| export default interface FilterConfigJson { | ||||
|     /** | ||||
|  | @ -34,7 +35,9 @@ export default interface FilterConfigJson { | |||
|      *     ``` | ||||
|      */ | ||||
|     options: { | ||||
|         question: string | any | ||||
|         question: Translatable | ||||
|         searchTerms?: Record<string, string[]> | ||||
|         icon?: string | ||||
|         osmTags?: TagConfigJson | ||||
|         default?: boolean | ||||
|         fields?: { | ||||
|  |  | |||
|  | @ -76,6 +76,7 @@ import { RecentSearch } from "../Logic/Geocoding/RecentSearch" | |||
| import PhotonSearch from "../Logic/Geocoding/PhotonSearch" | ||||
| import ThemeSearch from "../Logic/Geocoding/ThemeSearch" | ||||
| 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( | ||||
|             new CoordinateSearch(), | ||||
|             new LocalElementSearch(this, 5), | ||||
|             new OpenStreetMapIdSearch(this), | ||||
|             new PhotonSearch(), // new NominatimGeocoding(),
 | ||||
|             new FilterSearch(this), | ||||
|             //new LocalElementSearch(this, 5),
 | ||||
|             //new OpenStreetMapIdSearch(this),
 | ||||
|            // new PhotonSearch(), // new NominatimGeocoding(),
 | ||||
|             this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this) : undefined | ||||
|         ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
|       return | ||||
|     } | ||||
|     const v = value.data | ||||
|     for (let option of htmlElement.getElementsByTagName("option")) { | ||||
|     for (let option of Array.from(htmlElement.getElementsByTagName("option"))) { | ||||
|       if (option.value === v) { | ||||
|         option.selected = true | ||||
|         return | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ | |||
|   import FilterviewWithFields from "./FilterviewWithFields.svelte" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import { Utils } from "../../Utils" | ||||
| 
 | ||||
|   export let filteredLayer: FilteredLayer | ||||
|   export let highlightedLayer: Store<string | undefined> = new ImmutableStore(undefined) | ||||
|  | @ -28,7 +29,7 @@ | |||
|     return state.sync( | ||||
|       (f) => f === 0, | ||||
|       [], | ||||
|       (b) => (b ? 0 : undefined) | ||||
|       (b) => (b ? 0 : undefined), | ||||
|     ) | ||||
|   } | ||||
| 
 | ||||
|  | @ -75,6 +76,9 @@ | |||
|               <Dropdown value={getStateFor(filter)}> | ||||
|                 {#each filter.options as option, i} | ||||
|                   <option value={i}> | ||||
|                     {#if Utils.isEmoji(option.icon)} | ||||
|                       {option.icon} | ||||
|                     {/if} | ||||
|                     <Tr t={option.question} /> | ||||
|                   </option> | ||||
|                 {/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 Tr from "../Base/Tr.svelte" | ||||
|   import NextButton from "../Base/NextButton.svelte" | ||||
|   import Geosearch from "./Geosearch.svelte" | ||||
|   import Geosearch from "../Search/Geosearch.svelte" | ||||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ | |||
|   import { GeoOperations } from "../../Logic/GeoOperations" | ||||
|   import LocationInput from "../InputElement/Helpers/LocationInput.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 Constants from "../../Models/Constants" | ||||
|   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 { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding" | ||||
|   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 SearchResults from "./SearchResults.svelte" | ||||
|   import MoreScreen from "./MoreScreen" | ||||
|   import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||
|   import { focusWithArrows } from "../../Utils/focusWithArrows" | ||||
|   import ShowDataLayer from "../Map/ShowDataLayer" | ||||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
|   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 bounds: UIEventSource<BBox> | ||||
|  | @ -90,8 +88,7 @@ | |||
|         return | ||||
|       } | ||||
|       const poi = result[0] | ||||
|       if (poi.payload !== undefined) { | ||||
|         // This is a theme | ||||
|       if (poi.category === "theme") { | ||||
|         const theme = <MinimalLayoutInformation>poi.payload | ||||
|         const url = MoreScreen.createUrlFor(theme, false) | ||||
|         console.log("Found a theme, going to", url) | ||||
|  | @ -99,6 +96,9 @@ | |||
|         window.location = url | ||||
|         return | ||||
|       } | ||||
|       if(poi.category === "filter"){ | ||||
|         return  // Should not happen | ||||
|       } | ||||
|       if (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) { | ||||
|         return undefined | ||||
|       } | ||||
|       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)) | ||||
|   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 ThemeViewState from "../Models/ThemeViewState" | ||||
|   import type { MapProperties } from "../Models/MapProperties" | ||||
|   import Geosearch from "./BigComponents/Geosearch.svelte" | ||||
|   import Geosearch from "./Search/Geosearch.svelte" | ||||
|   import Translations from "./i18n/Translations" | ||||
|   import usersettings from "../assets/generated/layers/usersettings.json" | ||||
|   import { | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| import DOMPurify from "dompurify" | ||||
| 
 | ||||
| export class Utils { | ||||
|     /** | ||||
|      * In the 'deploy'-step, some code needs to be run by ts-node. | ||||
|  |  | |||
|  | @ -279,12 +279,12 @@ button.as-link { | |||
|     padding: 0; | ||||
| } | ||||
| 
 | ||||
| button.unstyled { | ||||
| button.unstyled, .button-unstyled button { | ||||
|     background-color: unset; | ||||
|     display: inline-flex; | ||||
|     justify-content: start; | ||||
|     border: none; | ||||
|     box-shadow: none; | ||||
|     box-shadow: none !important; | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue