forked from MapComplete/MapComplete
		
	Search: use 'searchbar' where applicable, refactoring
This commit is contained in:
		
							parent
							
								
									bcd53405c8
								
							
						
					
					
						commit
						9b8c300e77
					
				
					 28 changed files with 403 additions and 582 deletions
				
			
		|  | @ -166,7 +166,6 @@ | |||
|         } | ||||
|       ], | ||||
|       "allowMove": false | ||||
| 
 | ||||
|     }, | ||||
|     { | ||||
|       "id": "town_hall", | ||||
|  |  | |||
|  | @ -370,7 +370,6 @@ | |||
|         } | ||||
|       ], | ||||
|       "allowMove": false | ||||
| 
 | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  |  | |||
|  | @ -686,7 +686,6 @@ | |||
|         "enableImproveAccuraccy": true, | ||||
|         "enableRelocation": false | ||||
|       } | ||||
| 
 | ||||
|     }, | ||||
|     "named_streets" | ||||
|   ], | ||||
|  |  | |||
|  | @ -5161,6 +5161,18 @@ | |||
|                     "14": { | ||||
|                         "then": "Thai dishes are served here" | ||||
|                     }, | ||||
|                     "15": { | ||||
|                         "then": "Mexican dishes are served here" | ||||
|                     }, | ||||
|                     "16": { | ||||
|                         "then": "Japanese dishes are served here" | ||||
|                     }, | ||||
|                     "17": { | ||||
|                         "then": "Chicken based dishes are served here" | ||||
|                     }, | ||||
|                     "18": { | ||||
|                         "then": "Seafood dishes are served here" | ||||
|                     }, | ||||
|                     "2": { | ||||
|                         "then": "Mainly serves pasta" | ||||
|                     }, | ||||
|  | @ -5184,33 +5196,6 @@ | |||
|                     }, | ||||
|                     "9": { | ||||
|                         "then": "French dishes are served here" | ||||
|                     }, | ||||
|                     "10": { | ||||
|                         "then": "Chinese dishes are served here" | ||||
|                     }, | ||||
|                     "11": { | ||||
|                         "then": "Greek dishes are served here" | ||||
|                     }, | ||||
|                     "12": { | ||||
|                         "then": "Indian dishes are served here" | ||||
|                     }, | ||||
|                     "13": { | ||||
|                         "then": "Turkish dishes are served here" | ||||
|                     }, | ||||
|                     "14": { | ||||
|                         "then": "Thai dishes are served here" | ||||
|                     }, | ||||
|                     "15": { | ||||
|                         "then": "Mexican dishes are served here" | ||||
|                     }, | ||||
|                     "16": { | ||||
|                         "then": "Japanese dishes are served here" | ||||
|                     }, | ||||
|                     "17": { | ||||
|                         "then": "Chicken based dishes are served here" | ||||
|                     }, | ||||
|                     "18": { | ||||
|                         "then": "Seafood dishes are served here" | ||||
|                     } | ||||
|                 }, | ||||
|                 "question": "What kind of food is served here?", | ||||
|  | @ -12260,4 +12245,4 @@ | |||
|             "render": "wind turbine" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | @ -4028,6 +4028,19 @@ | |||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "10": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Geen voorkeur voor honden" | ||||
|                     }, | ||||
|                     "1": { | ||||
|                         "question": "Honden toegelaten" | ||||
|                     }, | ||||
|                     "2": { | ||||
|                         "question": "Geen honden toegelaten" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "2": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|  | @ -4084,19 +4097,6 @@ | |||
|                         "question": "Gratis toegankelijk" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "10": { | ||||
|                 "options": { | ||||
|                     "0": { | ||||
|                         "question": "Geen voorkeur voor honden" | ||||
|                     }, | ||||
|                     "1": { | ||||
|                         "question": "Honden toegelaten" | ||||
|                     }, | ||||
|                     "2": { | ||||
|                         "question": "Geen honden toegelaten" | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|  | @ -4305,6 +4305,18 @@ | |||
|                     "14": { | ||||
|                         "then": "Dit is een Thaïs restaurant" | ||||
|                     }, | ||||
|                     "15": { | ||||
|                         "then": "Dit is een mexicaans restaurant" | ||||
|                     }, | ||||
|                     "16": { | ||||
|                         "then": "Dit is een japans restaurant" | ||||
|                     }, | ||||
|                     "17": { | ||||
|                         "then": "Dit is een kiprestaurant" | ||||
|                     }, | ||||
|                     "18": { | ||||
|                         "then": "Dit is een vis- en zeerestaurant" | ||||
|                     }, | ||||
|                     "2": { | ||||
|                         "then": "Dit is een pastazaak" | ||||
|                     }, | ||||
|  | @ -4328,33 +4340,6 @@ | |||
|                     }, | ||||
|                     "9": { | ||||
|                         "then": "Dit is een Frans restaurant" | ||||
|                     }, | ||||
|                     "10": { | ||||
|                         "then": "Dit is een Chinees restaurant" | ||||
|                     }, | ||||
|                     "11": { | ||||
|                         "then": "Dit is een Grieks restaurant" | ||||
|                     }, | ||||
|                     "12": { | ||||
|                         "then": "Dit is een Indisch restaurant" | ||||
|                     }, | ||||
|                     "13": { | ||||
|                         "then": "Dit is een Turks restaurant (dat meer dan enkel kebab verkoopt)" | ||||
|                     }, | ||||
|                     "14": { | ||||
|                         "then": "Dit is een Thaïs restaurant" | ||||
|                     }, | ||||
|                     "15": { | ||||
|                         "then": "Dit is een mexicaans restaurant" | ||||
|                     }, | ||||
|                     "16": { | ||||
|                         "then": "Dit is een japans restaurant" | ||||
|                     }, | ||||
|                     "17": { | ||||
|                         "then": "Dit is een kiprestaurant" | ||||
|                     }, | ||||
|                     "18": { | ||||
|                         "then": "Dit is een vis- en zeerestaurant" | ||||
|                     } | ||||
|                 }, | ||||
|                 "question": "Welk soort gerechten worden hier geserveerd?", | ||||
|  | @ -9921,4 +9906,4 @@ | |||
|             "render": "windturbine" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | @ -1,6 +1,6 @@ | |||
| { | ||||
|   "name": "mapcomplete", | ||||
|   "version": "0.46.0", | ||||
|   "version": "0.47.0", | ||||
|   "repository": "https://github.com/pietervdvn/MapComplete", | ||||
|   "description": "A small website to edit OSM easily", | ||||
|   "bugs": "https://github.com/pietervdvn/MapComplete/issues", | ||||
|  |  | |||
|  | @ -1160,14 +1160,14 @@ input[type="range"].range-lg::-moz-range-thumb { | |||
|   left: 0px; | ||||
| } | ||||
| 
 | ||||
| .right-1\/3 { | ||||
|   right: 33.333333%; | ||||
| } | ||||
| 
 | ||||
| .right-0 { | ||||
|   right: 0px; | ||||
| } | ||||
| 
 | ||||
| .right-1\/3 { | ||||
|   right: 33.333333%; | ||||
| } | ||||
| 
 | ||||
| .right-10 { | ||||
|   right: 2.5rem; | ||||
| } | ||||
|  | @ -1413,6 +1413,11 @@ input[type="range"].range-lg::-moz-range-thumb { | |||
|   margin-right: auto; | ||||
| } | ||||
| 
 | ||||
| .mx-3 { | ||||
|   margin-left: 0.75rem; | ||||
|   margin-right: 0.75rem; | ||||
| } | ||||
| 
 | ||||
| .my-4 { | ||||
|   margin-top: 1rem; | ||||
|   margin-bottom: 1rem; | ||||
|  | @ -1474,6 +1479,14 @@ input[type="range"].range-lg::-moz-range-thumb { | |||
|   margin-bottom: 4rem; | ||||
| } | ||||
| 
 | ||||
| .mb-4 { | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
| 
 | ||||
| .ml-1 { | ||||
|   margin-left: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .mr-0\.5 { | ||||
|   margin-right: 0.125rem; | ||||
| } | ||||
|  | @ -1490,14 +1503,6 @@ input[type="range"].range-lg::-moz-range-thumb { | |||
|   margin-top: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .mb-4 { | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
| 
 | ||||
| .ml-1 { | ||||
|   margin-left: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .mt-2 { | ||||
|   margin-top: 0.5rem; | ||||
| } | ||||
|  | @ -1913,10 +1918,6 @@ input[type="range"].range-lg::-moz-range-thumb { | |||
|   max-height: 3rem; | ||||
| } | ||||
| 
 | ||||
| .max-h-screen { | ||||
|   max-height: 100vh; | ||||
| } | ||||
| 
 | ||||
| .max-h-full { | ||||
|   max-height: 100%; | ||||
| } | ||||
|  | @ -8090,14 +8091,14 @@ svg.apply-fill path { | |||
|     order: 9999; | ||||
|   } | ||||
| 
 | ||||
|   .sm\:m-2 { | ||||
|     margin: 0.5rem; | ||||
|   } | ||||
| 
 | ||||
|   .sm\:m-1 { | ||||
|     margin: 0.25rem; | ||||
|   } | ||||
| 
 | ||||
|   .sm\:m-2 { | ||||
|     margin: 0.5rem; | ||||
|   } | ||||
| 
 | ||||
|   .sm\:mx-1 { | ||||
|     margin-left: 0.25rem; | ||||
|     margin-right: 0.25rem; | ||||
|  |  | |||
|  | @ -1,12 +1,12 @@ | |||
| import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" | ||||
| import GeocodingProvider, { SearchResult, GeocodingOptions, GeocodeResult } from "./GeocodingProvider" | ||||
| import { Utils } from "../../Utils" | ||||
| import { Store, Stores } from "../UIEventSource" | ||||
| 
 | ||||
| export default class CombinedSearcher implements GeocodingProvider { | ||||
|     private _providers: ReadonlyArray<GeocodingProvider> | ||||
|     private _providersWithSuggest: ReadonlyArray<GeocodingProvider> | ||||
| export default class CombinedSearcher implements GeocodingProvider <GeocodeResult> { | ||||
|     private _providers: ReadonlyArray<GeocodingProvider<GeocodeResult>> | ||||
|     private _providersWithSuggest: ReadonlyArray<GeocodingProvider<GeocodeResult>> | ||||
| 
 | ||||
|     constructor(...providers: ReadonlyArray<GeocodingProvider>) { | ||||
|     constructor(...providers: ReadonlyArray<GeocodingProvider<GeocodeResult>>) { | ||||
|         this._providers = Utils.NoNull(providers) | ||||
|         this._providersWithSuggest = this._providers.filter(pr => pr.suggest !== undefined) | ||||
|     } | ||||
|  | @ -17,10 +17,13 @@ export default class CombinedSearcher implements GeocodingProvider { | |||
|      * @param geocoded | ||||
|      * @private | ||||
|      */ | ||||
|     private merge(geocoded: SearchResult[][]): SearchResult[] { | ||||
|         const results: SearchResult[] = [] | ||||
|     public static merge(geocoded: GeocodeResult[][]): GeocodeResult[] { | ||||
|         const results: GeocodeResult[] = [] | ||||
|         const seenIds = new Set<string>() | ||||
|         for (const geocodedElement of geocoded) { | ||||
|             if(geocodedElement === undefined){ | ||||
|                 continue | ||||
|             } | ||||
|             for (const entry of geocodedElement) { | ||||
| 
 | ||||
| 
 | ||||
|  | @ -40,13 +43,13 @@ export default class CombinedSearcher implements GeocodingProvider { | |||
| 
 | ||||
|     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { | ||||
|         const results = (await Promise.all(this._providers.map(pr => pr.search(query, options)))) | ||||
|         return this.merge(results) | ||||
|         return CombinedSearcher.merge(results) | ||||
|     } | ||||
| 
 | ||||
|     suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||
|         return Stores.concat( | ||||
|             this._providersWithSuggest.map(pr => pr.suggest(query, options))) | ||||
|             .map(gcrss => this.merge(gcrss)) | ||||
|             .map(gcrss => CombinedSearcher.merge(gcrss)) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import GeocodingProvider, { SearchResult } from "./GeocodingProvider" | ||||
| import GeocodingProvider, { GeocodeResult } from "./GeocodingProvider" | ||||
| import { Utils } from "../../Utils" | ||||
| import { ImmutableStore, Store } from "../UIEventSource" | ||||
| 
 | ||||
| /** | ||||
|  * A simple search-class which interprets possible locations | ||||
|  */ | ||||
| export default class CoordinateSearch implements GeocodingProvider { | ||||
| export default class CoordinateSearch implements GeocodingProvider<GeocodeResult> { | ||||
|     private static readonly latLonRegexes: ReadonlyArray<RegExp> = [ | ||||
|         /^(-?[0-9]+\.[0-9]+)[ ,;/\\]+(-?[0-9]+\.[0-9]+)/, | ||||
|         /lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lon[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/, | ||||
|  | @ -59,9 +59,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): SearchResult[] { | ||||
|     private directSearch(query: string): GeocodeResult[] { | ||||
| 
 | ||||
|         const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <SearchResult>{ | ||||
|         const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <GeocodeResult>{ | ||||
|             lat: Number(m[1]), | ||||
|             lon: Number(m[2]), | ||||
|             display_name: "lon: " + m[2] + ", lat: " + m[1], | ||||
|  | @ -71,7 +71,7 @@ export default class CoordinateSearch implements GeocodingProvider { | |||
| 
 | ||||
| 
 | ||||
|         const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r))) | ||||
|             .map(m => <SearchResult>{ | ||||
|             .map(m => <GeocodeResult>{ | ||||
|                 lat: Number(m[2]), | ||||
|                 lon: Number(m[1]), | ||||
|                 display_name: "lon: " + m[1] + ", lat: " + m[2], | ||||
|  | @ -81,11 +81,11 @@ export default class CoordinateSearch implements GeocodingProvider { | |||
|         return matches.concat(matchesLonLat) | ||||
|     } | ||||
| 
 | ||||
|     suggest(query: string): Store<SearchResult[]> { | ||||
|     suggest(query: string): Store<GeocodeResult[]> { | ||||
|         return new ImmutableStore(this.directSearch(query)) | ||||
|     } | ||||
| 
 | ||||
|     async search (query: string): Promise<SearchResult[]> { | ||||
|     async search (query: string): Promise<GeocodeResult[]> { | ||||
|         return this.directSearch(query) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,13 +3,15 @@ import GeocodingProvider, { FilterPayload, FilterResult, GeocodingOptions, Searc | |||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||
| import { Utils } from "../../Utils" | ||||
| import Locale from "../../UI/i18n/Locale" | ||||
| import Constants from "../../Models/Constants" | ||||
| 
 | ||||
| export default class FilterSearch implements GeocodingProvider { | ||||
|     private readonly _state: SpecialVisualizationState | ||||
|     private readonly suggestions | ||||
| 
 | ||||
|     constructor(state: SpecialVisualizationState) { | ||||
|         this._state = state | ||||
| 
 | ||||
|         this.suggestions = this.getSuggestions() | ||||
|     } | ||||
| 
 | ||||
|     async search(query: string): Promise<SearchResult[]> { | ||||
|  | @ -34,7 +36,6 @@ export default class FilterSearch implements GeocodingProvider { | |||
|             } | ||||
|             return query | ||||
|         }).filter(q => q.length > 0) | ||||
|         console.log("Queries:",queries) | ||||
|         const possibleFilters: FilterPayload[] = [] | ||||
|         for (const layer of this._state.layout.layers) { | ||||
|             if (!Array.isArray(layer.filters)) { | ||||
|  | @ -55,9 +56,9 @@ export default class FilterSearch implements GeocodingProvider { | |||
|                     terms = terms.map(t => Utils.simplifyStringForSearch(t)) | ||||
|                     terms.push(option.emoji) | ||||
|                     Utils.NoNullInplace(terms) | ||||
|                     const distances =    queries.flatMap(query => terms.map(entry => { | ||||
|                     const distances = queries.flatMap(query => terms.map(entry => { | ||||
|                         const d = Utils.levenshteinDistance(query, entry.slice(0, query.length)) | ||||
|                         console.log(query,"?  +",terms, "=",d) | ||||
|                         console.log(query, "?  +", terms, "=", d) | ||||
|                         const dRelative = d / query.length | ||||
|                         return dRelative | ||||
|                     })) | ||||
|  | @ -78,4 +79,37 @@ export default class FilterSearch implements GeocodingProvider { | |||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     getSuggestions(): FilterPayload[] { | ||||
|         if (this.suggestions) { | ||||
|        //     return this.suggestions
 | ||||
|         } | ||||
|         const result: FilterPayload[] = [] | ||||
|         for (const [id, filteredLayer] of this._state.layerState.filteredLayers) { | ||||
|             if (!Array.isArray(filteredLayer.layerDef.filters)) { | ||||
|                 continue | ||||
|             } | ||||
|             if (Constants.priviliged_layers.indexOf(id) >= 0) { | ||||
|                 continue | ||||
|             } | ||||
|             for (const filter of filteredLayer.layerDef.filters) { | ||||
|                 const singleFilterResults: FilterPayload[] = [] | ||||
|                 for (let i = 0; i < Math.min(filter.options.length, 5); i++) { | ||||
|                     const option = filter.options[i] | ||||
|                     if (option.osmTags === undefined) { | ||||
|                         continue | ||||
|                     } | ||||
|                     singleFilterResults.push({ | ||||
|                         option, | ||||
|                         filter, | ||||
|                         index: i, | ||||
|                         layer: filteredLayer.layerDef | ||||
|                     }) | ||||
|                 } | ||||
|                 Utils.shuffle(singleFilterResults) | ||||
|                 result.push(...singleFilterResults.slice(0,3)) | ||||
|             } | ||||
|         } | ||||
|         Utils.shuffle(result) | ||||
|         return result.slice(0,6) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -56,16 +56,16 @@ export interface GeocodingOptions { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| export default interface GeocodingProvider { | ||||
| export default interface GeocodingProvider<T extends SearchResult = SearchResult> { | ||||
| 
 | ||||
| 
 | ||||
|     search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> | ||||
|     search(query: string, options?: GeocodingOptions): Promise<T[]> | ||||
| 
 | ||||
|     /** | ||||
|      * @param query | ||||
|      * @param options | ||||
|      */ | ||||
|     suggest?(query: string, options?: GeocodingOptions): Store<SearchResult[]> | ||||
|     suggest?(query: string, options?: GeocodingOptions): Store<T[]> | ||||
| } | ||||
| 
 | ||||
| export type ReverseGeocodingResult = Feature<Geometry, { | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" | ||||
| import GeocodingProvider, { GeocodingOptions, GeocodeResult } from "./GeocodingProvider" | ||||
| import { OsmId } from "../../Models/OsmFeature" | ||||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||
| 
 | ||||
| export default class OpenStreetMapIdSearch implements GeocodingProvider { | ||||
|     private static readonly regex = /((https?:\/\/)?(www.)?(osm|openstreetmap).org\/)?(n|node|w|way|r|relation)[\/ ]?([0-9]+)/ | ||||
| export default class OpenStreetMapIdSearch implements GeocodingProvider<GeocodeResult> { | ||||
|     private static readonly regex = /((https?:\/\/)?(www.)?(osm|openstreetmap).org\/)?(n|node|w|way|r|relation)[/ ]?([0-9]+)/ | ||||
| 
 | ||||
|     private static readonly types: Readonly<Record<string, "node" | "way" | "relation">> = { | ||||
|         "n":"node", | ||||
|  | @ -45,7 +45,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider { | |||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|     async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> { | ||||
|     async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> { | ||||
|         const id = OpenStreetMapIdSearch.extractId(query) | ||||
|         if (!id) { | ||||
|             return [] | ||||
|  | @ -74,7 +74,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider { | |||
|         }] | ||||
|     } | ||||
| 
 | ||||
|     suggest?(query: string, options?: GeocodingOptions): Store<SearchResult[]> { | ||||
|     suggest?(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> { | ||||
|         return UIEventSource.FromPromise(this.search(query, options)) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,11 @@ | |||
| import GeocodingProvider, { FilterPayload, GeocodingUtils, type SearchResult } from "../Geocoding/GeocodingProvider" | ||||
| import GeocodingProvider, { | ||||
|     FilterPayload, | ||||
|     GeocodeResult, | ||||
|     GeocodingUtils, | ||||
|     type SearchResult | ||||
| } from "../Geocoding/GeocodingProvider" | ||||
| import { RecentSearch } from "../Geocoding/RecentSearch" | ||||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import CombinedSearcher from "../Geocoding/CombinedSearcher" | ||||
| import FilterSearch from "../Geocoding/FilterSearch" | ||||
| import LocalElementSearch from "../Geocoding/LocalElementSearch" | ||||
|  | @ -20,7 +25,6 @@ import ShowDataLayer from "../../UI/Map/ShowDataLayer" | |||
| export default class SearchState { | ||||
| 
 | ||||
|     public readonly isSearching = new UIEventSource(false) | ||||
|     public readonly geosearch: GeocodingProvider | ||||
|     public readonly recentlySearched: RecentSearch | ||||
|     public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) | ||||
|     public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("") | ||||
|  | @ -28,28 +32,40 @@ export default class SearchState { | |||
|     public readonly suggestions: Store<SearchResult[]> | ||||
|     public readonly filterSuggestions: Store<FilterPayload[]> | ||||
|     public readonly themeSuggestions: Store<MinimalLayoutInformation[]> | ||||
|     public readonly locationSearchers: ReadonlyArray<GeocodingProvider<GeocodeResult>> | ||||
| 
 | ||||
|     private readonly state: ThemeViewState | ||||
|     public readonly showSearchDrawer: UIEventSource<boolean> | ||||
|     public readonly suggestionsSearchRunning: Store<boolean> | ||||
| 
 | ||||
|     constructor(state: ThemeViewState) { | ||||
|         this.state = state | ||||
| 
 | ||||
|         this.geosearch = new CombinedSearcher( | ||||
|            // new LocalElementSearch(state, 5),
 | ||||
|         this.locationSearchers = [ | ||||
|             // new LocalElementSearch(state, 5),
 | ||||
|             new CoordinateSearch(), | ||||
|             new OpenStreetMapIdSearch(state), | ||||
|             new PhotonSearch() // new NominatimGeocoding(),
 | ||||
|         ) | ||||
|         ] | ||||
| 
 | ||||
|         this.recentlySearched = new RecentSearch(state) | ||||
|         const bounds = state.mapProperties.bounds | ||||
|         this.suggestions = this.searchTerm.stabilized(250).bindD(search => { | ||||
|         const suggestionsList = this.searchTerm.stabilized(250).mapD(search => { | ||||
|                 if (search.length === 0) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return Stores.holdDefined(bounds.bindD(bbox => this.geosearch.suggest(search, { bbox }))) | ||||
|                 return this.locationSearchers.map(ls => ls.suggest(search, { bbox: bounds.data })) | ||||
| 
 | ||||
|             }, [bounds] | ||||
|         ) | ||||
|         this.suggestionsSearchRunning =  suggestionsList.bind(suggestions => { | ||||
|             if(suggestions === undefined){ | ||||
|                 return new ImmutableStore(true) | ||||
|             } | ||||
|             return Stores.concat(suggestions).map(suggestions => suggestions.some(list => list === undefined)) | ||||
|         }) | ||||
|         this.suggestions = suggestionsList.bindD(suggestions => | ||||
|             Stores.concat(suggestions).map(suggestions => CombinedSearcher.merge(suggestions)) | ||||
|         ) | ||||
| 
 | ||||
|         const themeSearch = new ThemeSearch(state, 3) | ||||
|  | @ -57,8 +73,7 @@ export default class SearchState { | |||
| 
 | ||||
| 
 | ||||
|         const filterSearch = new FilterSearch(state) | ||||
|         this.filterSuggestions = this.searchTerm.stabilized(50).mapD(query => | ||||
|             filterSearch.searchDirectly(query) | ||||
|         this.filterSuggestions = this.searchTerm.stabilized(50).mapD(query => filterSearch.searchDirectly(query) | ||||
|         ).mapD(filterResult => { | ||||
|             const active = state.layerState.activeFilters.data | ||||
|             return filterResult.filter(({ filter, index, layer }) => { | ||||
|  | @ -81,11 +96,7 @@ export default class SearchState { | |||
|         ) | ||||
| 
 | ||||
|         this.showSearchDrawer = new UIEventSource(false) | ||||
|         this.suggestions.addCallbackAndRunD(sugg => { | ||||
|             if (sugg.length > 0) { | ||||
|                 this.showSearchDrawer.set(true) | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         this.searchIsFocused.addCallbackAndRunD(sugg => { | ||||
|             if (sugg) { | ||||
|                 this.showSearchDrawer.set(true) | ||||
|  |  | |||
|  | @ -41,17 +41,16 @@ export class Stores { | |||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     public static concat<T>(stores: Store<T[]>[]): Store<T[][]> { | ||||
|         const newStore = new UIEventSource<T[][]>([]) | ||||
|         function update(){ | ||||
|             if(newStore._callbacks.isDestroyed){ | ||||
|     public static concat<T>(stores: Store<T[] | undefined>[]): Store<(T[] | undefined)[]> { | ||||
|         const newStore = new UIEventSource<(T[] | undefined)[]>([]) | ||||
| 
 | ||||
|         function update() { | ||||
|             if (newStore._callbacks.isDestroyed) { | ||||
|                 return true // unregister
 | ||||
|             } | ||||
|             const results: T[][] = [] | ||||
|             const results: (T[] | undefined)[] = [] | ||||
|             for (const store of stores) { | ||||
|                 if(store.data){ | ||||
|                     results.push(store.data) | ||||
|                 } | ||||
|                 results.push(store.data) | ||||
|             } | ||||
|             newStore.setData(results) | ||||
|         } | ||||
|  | @ -261,7 +260,7 @@ export abstract class Store<T> implements Readable<T> { | |||
|                 if (mapped.data === newEventSource) { | ||||
|                     sink.setData(resultData) | ||||
|                 } | ||||
|                 if(sink._callbacks.isDestroyed){ | ||||
|                 if (sink._callbacks.isDestroyed) { | ||||
|                     return true // unregister
 | ||||
|                 } | ||||
|             }) | ||||
|  | @ -270,7 +269,7 @@ export abstract class Store<T> implements Readable<T> { | |||
|         return sink | ||||
|     } | ||||
| 
 | ||||
|     public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>, extraSources: UIEventSource<object>[] =[]): Store<X> { | ||||
|     public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>, extraSources: UIEventSource<object>[] = []): Store<X> { | ||||
|         return this.bind((t) => { | ||||
|             if (t === null) { | ||||
|                 return null | ||||
|  | @ -408,7 +407,8 @@ export class ImmutableStore<T> extends Store<T> { | |||
| class ListenerTracker<T> { | ||||
|     public pingCount = 0 | ||||
|     private readonly _callbacks: ((t: T) => boolean | void | any)[] = [] | ||||
| public isDestroyed = false | ||||
|     public isDestroyed = false | ||||
| 
 | ||||
|     /** | ||||
|      * Adds a callback which can be called; a function to unregister is returned | ||||
|      */ | ||||
|  | @ -469,8 +469,8 @@ public isDestroyed = false | |||
|         return this._callbacks.length | ||||
|     } | ||||
| 
 | ||||
|     public destroy(){ | ||||
|         this.isDestroyed=  true | ||||
|     public destroy() { | ||||
|         this.isDestroyed = true | ||||
|         this._callbacks.splice(0, this._callbacks.length) | ||||
|     } | ||||
| } | ||||
|  | @ -635,7 +635,8 @@ class MappedStore<TIn, T> extends Store<T> { | |||
| } | ||||
| 
 | ||||
| export class UIEventSource<T> extends Store<T> implements Writable<T> { | ||||
|     private static readonly pass: (() => void) = () => {}; | ||||
|     private static readonly pass: (() => void) = () => { | ||||
|     } | ||||
|     public data: T | ||||
|     _callbacks: ListenerTracker<T> = new ListenerTracker<T>() | ||||
| 
 | ||||
|  | @ -644,7 +645,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | |||
|         this.data = data | ||||
|     } | ||||
| 
 | ||||
|     public destroy(){ | ||||
|     public destroy() { | ||||
|         this._callbacks.destroy() | ||||
|     } | ||||
| 
 | ||||
|  | @ -782,9 +783,9 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | |||
|                     return defaultV | ||||
|                 } | ||||
|                 try { | ||||
|                     return <T> JSON.parse(str) | ||||
|                     return <T>JSON.parse(str) | ||||
|                 } catch (e) { | ||||
|                     console.error("Could not parse value", str,"due to",e) | ||||
|                     console.error("Could not parse value", str, "due to", e) | ||||
|                     return defaultV | ||||
|                 } | ||||
|             }, | ||||
|  |  | |||
|  | @ -2,11 +2,7 @@ import LayoutConfig from "./ThemeConfig/LayoutConfig" | |||
| import { SpecialVisualizationState } from "../UI/SpecialVisualization" | ||||
| import { Changes } from "../Logic/Osm/Changes" | ||||
| import { Store, UIEventSource } from "../Logic/UIEventSource" | ||||
| import { | ||||
|     FeatureSource, | ||||
|     IndexedFeatureSource, | ||||
|     WritableFeatureSource | ||||
| } from "../Logic/FeatureSource/FeatureSource" | ||||
| import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" | ||||
| import { OsmConnection } from "../Logic/Osm/OsmConnection" | ||||
| import { ExportableMap, MapProperties } from "./MapProperties" | ||||
| import LayerState from "../Logic/State/LayerState" | ||||
|  | @ -50,9 +46,7 @@ import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter" | |||
| import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" | ||||
| import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" | ||||
| import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" | ||||
| import NoElementsInViewDetector, { | ||||
|     FeatureViewState | ||||
| } from "../Logic/Actors/NoElementsInViewDetector" | ||||
| import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector" | ||||
| import FilteredLayer from "./FilteredLayer" | ||||
| import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector" | ||||
| import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" | ||||
|  | @ -70,19 +64,10 @@ import summaryLayer from "../assets/generated/layers/summary.json" | |||
| import last_click_layerconfig from "../assets/generated/layers/last_click.json" | ||||
| 
 | ||||
| import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson" | ||||
| import Locale from "../UI/i18n/Locale" | ||||
| import Hash from "../Logic/Web/Hash" | ||||
| import { GeoOperations } from "../Logic/GeoOperations" | ||||
| import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" | ||||
| import GeocodingProvider, { GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider" | ||||
| import CombinedSearcher from "../Logic/Geocoding/CombinedSearcher" | ||||
| import CoordinateSearch from "../Logic/Geocoding/CoordinateSearch" | ||||
| import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch" | ||||
| 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" | ||||
| import { GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider" | ||||
| import SearchState from "../Logic/State/SearchState" | ||||
| 
 | ||||
| /** | ||||
|  | @ -569,6 +554,10 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|                 this.previewedImage.setData(undefined) | ||||
|                 return | ||||
|             } | ||||
|             if(this.searchState.showSearchDrawer.data){ | ||||
|                 this.searchState.showSearchDrawer.set(false) | ||||
|                 return | ||||
|             } | ||||
|             if(this.guistate.closeAll()){ | ||||
|                return | ||||
|             } | ||||
|  | @ -623,6 +612,12 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|             ) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => { | ||||
|             this.searchState.feedback.set(undefined) | ||||
|             this.searchState.searchIsFocused.set(true) | ||||
|         }) | ||||
| 
 | ||||
|         this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => { | ||||
|             if (!enable) { | ||||
|                 return | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ | |||
|         rightOffset="inset-y-0 right-0" | ||||
|         bind:hidden={hidden}> | ||||
| 
 | ||||
|   <div class="normal-background h-screen"> | ||||
|   <div class="low-interaction h-screen"> | ||||
|     <div class="h-full" style={`padding-top: ${height}px`}> | ||||
|       <div class="flex flex-col h-full overflow-y-auto"> | ||||
|         <slot /> | ||||
|  |  | |||
|  | @ -15,18 +15,35 @@ | |||
|   $: value.set(_value) | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ search }>() | ||||
|   export let placeholder: Translation =  Translations.t.general.search.search | ||||
|   export let placeholder: Translation = Translations.t.general.search.search | ||||
|   export let isFocused: UIEventSource<boolean> = undefined | ||||
|   let inputElement: HTMLInputElement | ||||
| 
 | ||||
|   isFocused?.addCallback(focussed => { | ||||
|     if (focussed) { | ||||
|       requestAnimationFrame(() => { | ||||
|         if (document.activeElement !== inputElement) { | ||||
|           inputElement.focus() | ||||
|           inputElement.select() | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <form | ||||
|   class="flex justify-center" | ||||
|   class="w-full" | ||||
|   on:submit|preventDefault={() => dispatch("search")} | ||||
| > | ||||
|   <label | ||||
|     class="neutral-label my-2 flex w-full items-center rounded-full border-2 border-black sm:w-1/2  box-shadow" | ||||
|     class="neutral-label normal-background flex w-full items-center rounded-full border-2 border-black box-shadow" | ||||
|   > | ||||
|     <input | ||||
|       bind:this={inputElement} | ||||
|       on:focus={() => {isFocused?.setData(true)}} | ||||
|       on:blur={() => {isFocused?.setData(false)}} | ||||
|       type="search" | ||||
|       style=" --tw-ring-color: rgb(0 0 0 / 0) !important;" | ||||
|       class="ml-4 pl-1 w-full outline-none border-none" | ||||
|  | @ -35,9 +52,9 @@ | |||
|         }} | ||||
|       bind:value={_value} | ||||
|       use:set_placeholder={placeholder} | ||||
|       use:ariaLabel={Translations.t.general.search.search} | ||||
|       use:ariaLabel={placeholder} | ||||
|     /> | ||||
|     <SearchIcon aria-hidden="true" class="h-8 w-8 mx-2" /> | ||||
|     <SearchIcon aria-hidden="true" class="h-8 w-8 mx-3" /> | ||||
| 
 | ||||
|   </label> | ||||
| </form> | ||||
|  |  | |||
							
								
								
									
										55
									
								
								src/UI/Base/SidebarUnit.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/UI/Base/SidebarUnit.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | |||
| <div class="sidebar-unit"> | ||||
|   <slot/> | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|     :global(.sidebar-unit) { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         row-gap: 0.25rem; | ||||
|         background: var(--background-color); | ||||
|         padding: 0.5rem; | ||||
|         border-radius: 0.5rem; | ||||
|     } | ||||
| 
 | ||||
|     :global(.sidebar-unit > h3) { | ||||
|         margin-top: 0; | ||||
|         margin-bottom: 0.5rem; | ||||
|         padding: 0.25rem; | ||||
|     } | ||||
| 
 | ||||
|     :global(.sidebar-button svg, .sidebar-button img) { | ||||
|         width: 1.5rem; | ||||
|         height: 1.5rem; | ||||
|         margin-right: 0.5rem; | ||||
|         flex-shrink: 0; | ||||
|     } | ||||
| 
 | ||||
|     :global(.sidebar-button .weblate-link > svg) { | ||||
|         width: 0.75rem; | ||||
|         height: 0.75rem; | ||||
|         flex-shrink: 0; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     :global(.sidebar-button, .sidebar-unit > a) { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         border-radius: 0.25rem !important; | ||||
|         padding: 0.4rem 0.75rem !important; | ||||
|         text-decoration: none !important; | ||||
|         width: 100%; | ||||
|         text-align: start; | ||||
|     } | ||||
| 
 | ||||
|     :global(.sidebar-button > svg , .sidebar-button > img, .sidebar-unit > a img, .sidebar-unit > a svg) { | ||||
|         margin-right: 0.5rem; | ||||
|         flex-shrink: 0; | ||||
|     } | ||||
| 
 | ||||
|     :global(.sidebar-button:hover, .sidebar-unit > a:hover) { | ||||
|         background: var(--low-interaction-background) !important; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| </style> | ||||
|  | @ -11,7 +11,7 @@ | |||
|   import CommunityIndexView from "./CommunityIndexView.svelte" | ||||
|   import Community from "../../assets/svg/Community.svelte" | ||||
|   import LoginToggle from "../Base/LoginToggle.svelte" | ||||
|   import { CloseButton, Sidebar } from "flowbite-svelte" | ||||
|   import { CloseButton } from "flowbite-svelte" | ||||
|   import HotkeyTable from "./HotkeyTable.svelte" | ||||
|   import { Utils } from "../../Utils" | ||||
|   import Constants from "../../Models/Constants" | ||||
|  | @ -24,7 +24,6 @@ | |||
|   import MapillaryLink from "./MapillaryLink.svelte" | ||||
|   import Github from "../../assets/svg/Github.svelte" | ||||
|   import Bug from "../../assets/svg/Bug.svelte" | ||||
|   import Add from "../../assets/svg/Add.svelte" | ||||
|   import CopyrightPanel from "./CopyrightPanel.svelte" | ||||
|   import CopyrightAllIcons from "./CopyrightAllIcons.svelte" | ||||
|   import LanguagePicker from "../InputElement/LanguagePicker.svelte" | ||||
|  | @ -49,6 +48,7 @@ | |||
|   import Copyright from "../../assets/svg/Copyright.svelte" | ||||
|   import Pencil from "../../assets/svg/Pencil.svelte" | ||||
|   import Squares2x2 from "@babeard/svelte-heroicons/mini/Squares2x2" | ||||
|   import SidebarUnit from "../Base/SidebarUnit.svelte" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
|   let userdetails = state.osmConnection.userDetails | ||||
|  | @ -83,7 +83,7 @@ | |||
| 
 | ||||
| 
 | ||||
|   <!-- User related: avatar, settings, favourits, logout --> | ||||
|   <div class="sidebar-unit"> | ||||
|   <SidebarUnit> | ||||
|     <LoginToggle {state}> | ||||
|       <LoginButton osmConnection={state.osmConnection} slot="not-logged-in"></LoginButton> | ||||
|       <div class="flex gap-x-4 items-center"> | ||||
|  | @ -153,11 +153,11 @@ | |||
| 
 | ||||
|     <LanguagePicker /> | ||||
| 
 | ||||
|   </div> | ||||
|   </SidebarUnit> | ||||
| 
 | ||||
| 
 | ||||
|   <!-- Theme related: documentation links, download, ... --> | ||||
|   <div class="sidebar-unit"> | ||||
|   <SidebarUnit> | ||||
|     <h3> | ||||
|       <Tr t={t.aboutCurrentThemeTitle} /> | ||||
|     </h3> | ||||
|  | @ -218,11 +218,11 @@ | |||
|         <Tr t={Translations.t.general.attribution.openOsmcha.Subs({ theme: layout.title })} /> | ||||
|       </a> | ||||
|     {/if} | ||||
|   </div> | ||||
|   </SidebarUnit> | ||||
| 
 | ||||
| 
 | ||||
|   <!-- Other links and tools for the given location: open iD/JOSM; community index, ... --> | ||||
|   <div class="sidebar-unit"> | ||||
|   <SidebarUnit> | ||||
| 
 | ||||
|     <h3> | ||||
|       <Tr t={t.moreUtilsTitle} /> | ||||
|  | @ -244,11 +244,11 @@ | |||
|       <MapillaryLink large={false} mapProperties={state.mapProperties} /> | ||||
|     </If> | ||||
| 
 | ||||
|   </div> | ||||
|   </SidebarUnit> | ||||
| 
 | ||||
| 
 | ||||
|   <!-- About MC: various outward links, legal info, ... --> | ||||
|   <div class="sidebar-unit"> | ||||
|   <SidebarUnit> | ||||
| 
 | ||||
|     <h3> | ||||
|       <Tr t={Translations.t.general.menu.aboutMapComplete} /> | ||||
|  | @ -325,58 +325,8 @@ | |||
|     <div class="subtle self-end"> | ||||
|       {Constants.vNumber} | ||||
|     </div> | ||||
|   </div> | ||||
|   </SidebarUnit> | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <style> | ||||
|     :global(.sidebar-unit) { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         row-gap: 0.25rem; | ||||
|         background: var(--background-color); | ||||
|         padding: 0.5rem; | ||||
|         border-radius: 0.5rem; | ||||
|     } | ||||
| 
 | ||||
|     :global(.sidebar-unit > h3) { | ||||
|         margin-top: 0; | ||||
|         margin-bottom: 0.5rem; | ||||
|         padding: 0.25rem; | ||||
|     } | ||||
| 
 | ||||
|     :global(.sidebar-button svg, .sidebar-button img) { | ||||
|         width: 1.5rem; | ||||
|         height: 1.5rem; | ||||
|         margin-right: 0.5rem; | ||||
|         flex-shrink: 0; | ||||
|     } | ||||
| 
 | ||||
|     :global(.sidebar-button .weblate-link > svg) { | ||||
|         width: 0.75rem; | ||||
|         height: 0.75rem; | ||||
|         flex-shrink: 0; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     :global(.sidebar-button, .sidebar-unit > a) { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         border-radius: 0.25rem !important; | ||||
|         padding: 0.4rem 0.75rem !important; | ||||
|         text-decoration: none !important; | ||||
|         width: 100%; | ||||
|         text-align: start; | ||||
|     } | ||||
| 
 | ||||
|     :global(.sidebar-button > svg , .sidebar-button > img, .sidebar-unit a > img, .sidebar-unit > a svg) { | ||||
|         margin-right: 0.5rem; | ||||
|         flex-shrink: 0; | ||||
|     } | ||||
| 
 | ||||
|     :global(.sidebar-button:hover, .sidebar-unit > a:hover) { | ||||
|         background: var(--low-interaction-background) !important; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,59 +0,0 @@ | |||
| <script lang="ts"> | ||||
|   import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
|   import Hotkeys from "../Base/Hotkeys" | ||||
|   import { createEventDispatcher, onDestroy } from "svelte" | ||||
|   import { placeholder } from "../../Utils/placeholder" | ||||
|   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { ariaLabel } from "../../Utils/ariaLabel" | ||||
|   import { Translation } from "../i18n/Translation" | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ search: string }>() | ||||
| 
 | ||||
|   export let searchValue: UIEventSource<string> | ||||
|   export let placeholderText: Translation = Translations.t.general.search.search | ||||
|   export let feedback = new UIEventSource<string>(undefined) | ||||
| 
 | ||||
|   let isRunning: boolean = false | ||||
| 
 | ||||
|   let inputElement: HTMLInputElement | ||||
| 
 | ||||
|   function _performSearch() { | ||||
|     dispatch("search", searchValue.data) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <div class="normal-background flex justify-between rounded-full"> | ||||
|   <form class="flex w-full flex-wrap" on:submit|preventDefault={() => {}}> | ||||
|     {#if isRunning} | ||||
|       <Loading>{Translations.t.general.search.searching}</Loading> | ||||
|     {:else} | ||||
|       <div class="flex w-full rounded-full border border-gray-300"> | ||||
|         <input | ||||
|           type="search" | ||||
|           class="mx-2 w-full outline-none" | ||||
|           bind:this={inputElement} | ||||
|           on:keypress={(keypr) => { | ||||
|             feedback.set(undefined) | ||||
|             return keypr.key === "Enter" ? _performSearch() : undefined | ||||
|           }} | ||||
|           bind:value={$searchValue} | ||||
|           use:placeholder={placeholderText} | ||||
|           use:ariaLabel={Translations.t.general.search.search} | ||||
|         /> | ||||
|         <SearchIcon | ||||
|           aria-hidden="true" | ||||
|           class="h-6 w-6 self-end" | ||||
|           on:click={(event) => _performSearch()} | ||||
|         /> | ||||
|       </div> | ||||
|       {#if $feedback !== undefined} | ||||
|         <!-- The feedback is _always_ shown for screenreaders and to make sure that the searchfield can still be selected by tabbing--> | ||||
|         <div class="alert" role="alert" aria-live="assertive"> | ||||
|           {$feedback} | ||||
|         </div> | ||||
|       {/if} | ||||
|     {/if} | ||||
|   </form> | ||||
| </div> | ||||
|  | @ -8,11 +8,11 @@ | |||
|   import { ImmutableStore, Store, Stores, UIEventSource } from "../../../Logic/UIEventSource" | ||||
|   import Wikidata, { WikidataResponse } from "../../../Logic/Web/Wikidata" | ||||
|   import Locale from "../../i18n/Locale" | ||||
|   import SearchField from "../../BigComponents/SearchField.svelte" | ||||
|   import Loading from "../../Base/Loading.svelte" | ||||
|   import Wikidatapreview from "../../Wikipedia/Wikidatapreview.svelte" | ||||
|   import { Utils } from "../../../Utils" | ||||
|   import WikidataValidator from "../Validators/WikidataValidator" | ||||
|   import Searchbar from "../../Base/Searchbar.svelte" | ||||
| 
 | ||||
|   const t = Translations.t.general.wikipedia | ||||
| 
 | ||||
|  | @ -89,7 +89,7 @@ | |||
| </h3> | ||||
| 
 | ||||
| <form> | ||||
|   <SearchField {searchValue} placeholderText={placeholder} /> | ||||
|   <Searchbar value={searchValue} {placeholder} /> | ||||
| 
 | ||||
|   {#if $searchValue.trim().length === 0} | ||||
|     <Tr cls="w-full flex justify-center p-4" t={t.doSearch} /> | ||||
|  |  | |||
|  | @ -12,12 +12,10 @@ | |||
|   import { GeoOperations } from "../../Logic/GeoOperations" | ||||
|   import LocationInput from "../InputElement/Helpers/LocationInput.svelte" | ||||
|   import OpenBackgroundSelectorButton from "../BigComponents/OpenBackgroundSelectorButton.svelte" | ||||
|   import Geosearch from "../Search/Geosearch.svelte" | ||||
|   import If from "../Base/If.svelte" | ||||
|   import Constants from "../../Models/Constants" | ||||
|   import LoginToggle from "../Base/LoginToggle.svelte" | ||||
|   import AccordionSingle from "../Flowbite/AccordionSingle.svelte" | ||||
|   import BackButton from "../Base/BackButton.svelte" | ||||
|   import ChevronLeft from "@babeard/svelte-heroicons/solid/ChevronLeft" | ||||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
|   import Icon from "../Map/Icon.svelte" | ||||
|  | @ -104,7 +102,7 @@ | |||
|             </div> | ||||
| 
 | ||||
|             {#if $reason.includeSearch} | ||||
|               <Geosearch {state}/> | ||||
|              <!-- TODO --> | ||||
|             {/if} | ||||
| 
 | ||||
|             <div class="flex flex-wrap"> | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ | |||
| {#if loading} | ||||
|   <Loading /> | ||||
| {:else } | ||||
|   <div class="badge"> | ||||
|   <div class="badge button-unstyled w-fit"> | ||||
|     <FilterOption option={$option} /> | ||||
|     <button on:click={() => clear()}> | ||||
|       <XMarkIcon class="w-5 h-5 pl-1" color="gray" /> | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ | |||
|   import { default as ActiveFilterSvelte } from "./ActiveFilter.svelte" | ||||
|   import type { ActiveFilter } from "../../Logic/State/LayerState" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
|   import SidebarUnit from "../Base/SidebarUnit.svelte" | ||||
| 
 | ||||
|   export let activeFilters: ActiveFilter[] | ||||
|   let loading = false | ||||
|  | @ -18,20 +19,25 @@ | |||
|   } | ||||
| </script> | ||||
| {#if activeFilters.length > 0} | ||||
|   <div class="flex flex-wrap gap-y-1 gap-x-1 button-unstyled"> | ||||
|   <SidebarUnit> | ||||
|     <h3>Active filters</h3> | ||||
| 
 | ||||
|     {#if loading} | ||||
|       <Loading /> | ||||
|     {:else} | ||||
|       {#each activeFilters as activeFilter (activeFilter)} | ||||
|         <ActiveFilterSvelte {activeFilter} /> | ||||
|       {/each} | ||||
|       <div class="flex flex-wrap gap-x-1 gap-y-2"> | ||||
| 
 | ||||
|       <button class="as-link subtle" on:click={() => clear()}> | ||||
|       {#each activeFilters as activeFilter (activeFilter)} | ||||
|         <div> | ||||
|         <ActiveFilterSvelte {activeFilter} /> | ||||
|         </div> | ||||
|       {/each} | ||||
|       </div> | ||||
| 
 | ||||
|       <button class="as-link subtle self-end" on:click={() => clear()} style="margin-right: 0.75rem"> | ||||
|         Clear filters | ||||
|       </button> | ||||
|     {/if} | ||||
|   </div> | ||||
|   </SidebarUnit> | ||||
| 
 | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,95 +0,0 @@ | |||
| <script lang="ts"> | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
|   import Hotkeys from "../Base/Hotkeys" | ||||
|   import { createEventDispatcher, onDestroy } from "svelte" | ||||
|   import { placeholder } from "../../Utils/placeholder" | ||||
|   import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { ariaLabel } from "../../Utils/ariaLabel" | ||||
| 
 | ||||
|   import { focusWithArrows } from "../../Utils/focusWithArrows" | ||||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
|   export let searchContents: UIEventSource<string> = new UIEventSource<string>("") | ||||
| 
 | ||||
|   function performSearch() { | ||||
|     state.searchState.performSearch() | ||||
|   } | ||||
| 
 | ||||
|   let isRunning = state.searchState.isSearching | ||||
| 
 | ||||
|   let inputElement: HTMLInputElement | ||||
| 
 | ||||
|   export let isFocused = new UIEventSource(false) | ||||
| 
 | ||||
|   function focusOnSearch() { | ||||
|     requestAnimationFrame(() => { | ||||
|       inputElement?.focus() | ||||
|       inputElement?.select() | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => { | ||||
|     state.searchState.feedback.set(undefined) | ||||
|     focusOnSearch() | ||||
|   }) | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ searchCompleted; searchIsValid: boolean }>() | ||||
|   $: { | ||||
|     if (!$searchContents?.trim()) { | ||||
|       dispatch("searchIsValid", false) | ||||
|     } else { | ||||
|       dispatch("searchIsValid", true) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   let geosearch: HTMLDivElement | ||||
| 
 | ||||
|   function checkFocus() { | ||||
|     window.requestAnimationFrame(() => { | ||||
|       if (geosearch?.contains(document.activeElement)) { | ||||
|         return | ||||
|       } | ||||
|       isFocused.setData(false) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   document.addEventListener("focus", () => { | ||||
|     checkFocus() | ||||
|   }, true /* use 'capturing' instead of bubbling, needed for focus-events*/) | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div bind:this={geosearch} use:focusWithArrows={"searchresult"}> | ||||
| 
 | ||||
|   <div class="normal-background flex justify-between rounded-full pl-2 w-full"> | ||||
|     <form class="flex w-full flex-wrap"> | ||||
|       {#if $isRunning} | ||||
|         <Loading>{Translations.t.general.search.searching}</Loading> | ||||
|       {:else} | ||||
|         <input | ||||
|           type="search" | ||||
|           class="w-full outline-none" | ||||
|           bind:this={inputElement} | ||||
|           on:keypress={(keypr) => { | ||||
|             if(keypr.key === "Enter"){ | ||||
|               performSearch() | ||||
|               keypr.preventDefault() | ||||
|             } | ||||
|             return undefined | ||||
|         }} | ||||
|           on:focus={() => {isFocused.setData(true)}} | ||||
|           on:blur={() => {checkFocus()}} | ||||
|           bind:value={$searchContents} | ||||
|           use:placeholder={Translations.t.general.search.search} | ||||
|           use:ariaLabel={Translations.t.general.search.search} | ||||
|         /> | ||||
| 
 | ||||
|       {/if} | ||||
|     </form> | ||||
|     <SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={() => performSearch()} /> | ||||
|   </div> | ||||
| </div> | ||||
|  | @ -13,87 +13,103 @@ | |||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
|   import FilterResult from "./FilterResult.svelte" | ||||
|   import ThemeResult from "./ThemeResult.svelte" | ||||
|   import SidebarUnit from "../Base/SidebarUnit.svelte" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
|   let activeFilters: Store<ActiveFilter[]> = state.layerState.activeFilters.map(fs => fs.filter(f => Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0)) | ||||
|   let recentlySeen: Store<GeocodeResult[]> = state.searchState.recentlySearched.seenThisSession | ||||
|   let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3)) | ||||
|   let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 6)) | ||||
|   let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview | ||||
|   let searchTerm = state.searchState.searchTerm | ||||
|   let results = state.searchState.suggestions | ||||
|   let isSearching = state.searchState.suggestionsSearchRunning | ||||
|   let filterResults = state.searchState.filterSuggestions | ||||
|   let themeResults = state.searchState.themeSuggestions | ||||
| 
 | ||||
| </script> | ||||
| <div class="p-4"> | ||||
| <div class="p-4 low-interaction flex gap-y-2 flex-col"> | ||||
| 
 | ||||
|   <h3>Search results</h3> | ||||
| 
 | ||||
|   <ActiveFilters activeFilters={$activeFilters} /> | ||||
| 
 | ||||
|   {#if $filterResults.length > 0} | ||||
|     <h3>Pick a filter below</h3> | ||||
|   {#if $searchTerm.length > 0 && $filterResults.length > 0} | ||||
|     <SidebarUnit> | ||||
| 
 | ||||
|     <div class="flex flex-wrap"> | ||||
|       {#each $filterResults as filterResult (filterResult)} | ||||
|         <FilterResult {state} entry={filterResult} /> | ||||
|       {/each} | ||||
|     </div> | ||||
|       <h3>Pick a filter below</h3> | ||||
| 
 | ||||
|       <div class="flex flex-wrap"> | ||||
|         {#each $filterResults as filterResult (filterResult)} | ||||
|           <FilterResult {state} entry={filterResult} /> | ||||
|         {/each} | ||||
|       </div> | ||||
|     </SidebarUnit> | ||||
|   {/if} | ||||
| 
 | ||||
|   <!-- Actual search results (or ""loading"", or ""no results"")--> | ||||
|   {#if $searchTerm.length > 0} | ||||
|     <h3>Locations</h3> | ||||
|   {/if} | ||||
|   {#if $searchTerm.length > 0 && $results === undefined} | ||||
|     <div class="flex justify-center m-4 my-8"> | ||||
|       <Loading /> | ||||
|     </div> | ||||
|   {:else if $results?.length > 0} | ||||
|     {#each $results as entry (entry)} | ||||
|       <SearchResultSvelte on:select {entry} {state} /> | ||||
|     {/each} | ||||
|   {:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0} | ||||
|     <div class="flex flex-col gap-y-8" | ||||
|          tabindex="-1"> | ||||
|       {#if $searchTerm.length > 0} | ||||
|     <SidebarUnit> | ||||
| 
 | ||||
|       <h3>Locations</h3> | ||||
| 
 | ||||
|       {#if $isSearching} | ||||
|         <div class="flex justify-center m-4 my-8"> | ||||
|           <Loading /> | ||||
|         </div> | ||||
|       {/if} | ||||
| 
 | ||||
|       {#if $results?.length > 0} | ||||
|         {#each $results as entry (entry)} | ||||
|           <SearchResultSvelte on:select {entry} {state} /> | ||||
|         {/each} | ||||
| 
 | ||||
|       {:else if !$isSearching} | ||||
|         <b class="flex justify-center p-4"> | ||||
|           <Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} /> | ||||
|         </b> | ||||
|       {/if} | ||||
|     </SidebarUnit> | ||||
| 
 | ||||
|       {#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), osm_id: themeId, category: "theme"}} | ||||
|               {state} | ||||
|               on:select /> | ||||
|           {/each} | ||||
|         </div> | ||||
|       {/if} | ||||
|     </div> | ||||
|   {/if} | ||||
| 
 | ||||
| 
 | ||||
|   <!-- Other maps which match the search term--> | ||||
|   {#if $themeResults.length > 0} | ||||
|     <h3> | ||||
|       Other maps | ||||
|     </h3> | ||||
|     {#each $themeResults as entry} | ||||
|       <ThemeResult {state} {entry} /> | ||||
|     {/each} | ||||
|     <SidebarUnit> | ||||
|       <h3> | ||||
|         Other maps | ||||
|       </h3> | ||||
|       {#each $themeResults as entry} | ||||
|         <ThemeResult {entry} /> | ||||
|       {/each} | ||||
|     </SidebarUnit> | ||||
|   {/if} | ||||
| 
 | ||||
| 
 | ||||
|   {#if $searchTerm.length == 0 && $recentlySeen?.length > 0} | ||||
|     <SidebarUnit> | ||||
|       <h3 class="m-2"> | ||||
|         <Tr t={Translations.t.general.search.recents} /> | ||||
|       </h3> | ||||
|       {#each $recentlySeen as entry} | ||||
|         <SearchResultSvelte {entry} {state} on:select /> | ||||
|       {/each} | ||||
|     </SidebarUnit> | ||||
|   {/if} | ||||
| 
 | ||||
|   {#if $searchTerm.length === 0 && $recentThemes?.length > 0 && $allowOtherThemes} | ||||
|     <SidebarUnit> | ||||
|       <h3 class="m-2"> | ||||
|         <Tr t={Translations.t.general.search.recentThemes} /> | ||||
|       </h3> | ||||
|       {#each $recentThemes as themeId (themeId)} | ||||
|         <SearchResultSvelte | ||||
|           entry={{payload: MoreScreen.officialThemesById.get(themeId), osm_id: themeId, category: "theme"}} | ||||
|           {state} | ||||
|           on:select /> | ||||
|       {/each} | ||||
|     </SidebarUnit> | ||||
|   {/if} | ||||
| 
 | ||||
| 
 | ||||
| </div> | ||||
|  |  | |||
|  | @ -11,7 +11,6 @@ | |||
|   import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||
|   import ThemeViewState from "../Models/ThemeViewState" | ||||
|   import type { MapProperties } from "../Models/MapProperties" | ||||
|   import Geosearch from "./Search/Geosearch.svelte" | ||||
|   import Translations from "./i18n/Translations" | ||||
|   import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import Tr from "./Base/Tr.svelte" | ||||
|  | @ -47,6 +46,7 @@ | |||
|   import SearchResults from "./Search/SearchResults.svelte" | ||||
|   import { CloseButton } from "flowbite-svelte" | ||||
|   import Hash from "../Logic/Web/Hash" | ||||
|   import Searchbar from "./Base/Searchbar.svelte" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
|   let layout = state.layout | ||||
|  | @ -161,10 +161,25 @@ | |||
| </script> | ||||
| 
 | ||||
| <main> | ||||
|   <!-- Main map --> | ||||
|   <div class="absolute top-0 left-0 h-screen w-screen overflow-hidden"> | ||||
|     <MaplibreMap map={maplibremap} mapProperties={mapproperties} autorecovery={true} /> | ||||
|   </div> | ||||
| 
 | ||||
|   <LoginToggle ignoreLoading={true} {state}> | ||||
|     {#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $visualFeedback} | ||||
|       <!-- Don't use h-full: h-full does _not_ include the area under the URL-bar, which offsets the crosshair a bit --> | ||||
|       <div | ||||
|         class="pointer-events-none absolute top-0 left-0 flex w-full items-center justify-center" | ||||
|         style="height: 100vh" | ||||
|       > | ||||
|         <Cross class="h-4 w-4" /> | ||||
|       </div> | ||||
|     {/if} | ||||
|     <!-- Add in an empty container to remove error messages if login fails --> | ||||
|     <svelte:fragment slot="error" /> | ||||
|   </LoginToggle> | ||||
| 
 | ||||
|   {#if $visualFeedback} | ||||
|     <div | ||||
|       class="pointer-events-none absolute top-0 left-0 flex h-screen w-screen items-center justify-center overflow-hidden" | ||||
|  | @ -177,108 +192,6 @@ | |||
|     </div> | ||||
|   {/if} | ||||
| 
 | ||||
|   <div class="pointer-events-none absolute top-0 left-0 w-full"> | ||||
|     <!-- Top components --> | ||||
| 
 | ||||
|     <div | ||||
|       class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap-reverse"> | ||||
|       <!-- Top bar with tools --> | ||||
|       <div class="flex items-center"> | ||||
| 
 | ||||
|         <MapControlButton | ||||
|           cls="m-0.5 p-0.5 sm:p-1" | ||||
|           arialabel={Translations.t.general.labels.menu} | ||||
|           on:click={() => {console.log("Opening...."); state.guistate.pageStates.menu.setData(true)}} | ||||
|           on:keydown={forwardEventToMap} | ||||
|         > | ||||
|           <MenuIcon class="h-6 w-6 cursor-pointer" /> | ||||
|         </MapControlButton> | ||||
| 
 | ||||
|         <MapControlButton | ||||
|           on:click={() => state.guistate.pageStates.about_theme.set(true)} | ||||
|           on:keydown={forwardEventToMap} | ||||
|         > | ||||
|           <div | ||||
|             class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 mr-2" | ||||
|           > | ||||
|             <Marker icons={layout.icon} size="h-6 w-6 shrink-0 mr-0.5 sm:mr-1 md:mr-2" /> | ||||
|             <b class="mr-1"> | ||||
|               <Tr t={layout.title} /> | ||||
|             </b> | ||||
|           </div> | ||||
|         </MapControlButton> | ||||
|       </div> | ||||
| 
 | ||||
|       {#if $debug && $hash} | ||||
|         <div class="alert"> | ||||
|           {$hash} | ||||
|         </div> | ||||
|       {/if} | ||||
| 
 | ||||
|       <If condition={state.featureSwitches.featureSwitchSearch}> | ||||
|         <div class="w-full sm:w-64 my-2 sm:mt-0"> | ||||
| 
 | ||||
|           <Geosearch | ||||
|             bounds={state.mapProperties.bounds} | ||||
|             on:searchCompleted={() => { | ||||
|             state.map?.data?.getCanvas()?.focus() | ||||
|           }} | ||||
|             perLayer={state.perLayer} | ||||
|             selectedElement={state.selectedElement} | ||||
|             geolocationState={state.geolocation.geolocationState} | ||||
|           /> | ||||
|         </div> | ||||
|       </If> | ||||
| 
 | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2"> | ||||
|       <If condition={state.visualFeedback}> | ||||
|         {#if $selectedElement === undefined} | ||||
|           <div class="w-fit"> | ||||
|             <VisualFeedbackPanel {state} /> | ||||
|           </div> | ||||
|         {/if} | ||||
|       </If> | ||||
| 
 | ||||
|     </div> | ||||
|     <div class="float-left m-1 flex flex-col sm:mt-2"> | ||||
|       <If condition={state.featureSwitches.featureSwitchWelcomeMessage}> | ||||
| 
 | ||||
| 
 | ||||
|       </If> | ||||
|       {#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()} | ||||
|         <MapControlButton | ||||
|           on:click={() => { | ||||
|             state.selectCurrentView() | ||||
|           }} | ||||
|           on:keydown={forwardEventToMap} | ||||
|         > | ||||
|           <div class="h-8 w-8 cursor-pointer"> | ||||
|             <ToSvelte construct={() => currentViewLayer.defaultIcon()} /> | ||||
|           </div> | ||||
|         </MapControlButton> | ||||
|       {/if} | ||||
|       <ExtraLinkButton {state} /> | ||||
|       <UploadingImageCounter featureId="*" showThankYou={false} {state} /> | ||||
|       <PendingChangesIndicator {state} /> | ||||
|       <If condition={state.featureSwitchIsTesting}> | ||||
|         <div class="alert w-fit">Testmode</div> | ||||
|       </If> | ||||
|       {#if state.osmConnection.Backend().startsWith("https://master.apis.dev.openstreetmap.org")} | ||||
|         <div class="thanks">Testserver</div> | ||||
|       {/if} | ||||
|       <If condition={state.featureSwitches.featureSwitchFakeUser}> | ||||
|         <div class="alert w-fit">Faking a user (Testmode)</div> | ||||
|       </If> | ||||
|     </div> | ||||
|     <div class="flex w-full flex-col items-center justify-center"> | ||||
|       <!-- Flex and w-full are needed for the positioning --> | ||||
|       <!-- Centermessage --> | ||||
|       <StateIndicator {state} /> | ||||
|       <ReverseGeocoding {state} /> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen"> | ||||
|     <!-- bottom controls --> | ||||
|  | @ -389,7 +302,7 @@ | |||
|   </div> | ||||
| 
 | ||||
| 
 | ||||
|   <DrawerRight shown={state.searchState.showSearchDrawer} }> | ||||
|   <DrawerRight shown={state.searchState.showSearchDrawer}> | ||||
|     <div class="relative"> | ||||
|       <div class="absolute right-0 top-0 "> | ||||
|         <div class="mr-4 mt-4"> | ||||
|  | @ -401,8 +314,11 @@ | |||
|   </DrawerRight> | ||||
| 
 | ||||
| 
 | ||||
|   <div class="pointer-events-none absolute top-0 left-0 w-full"> | ||||
| 
 | ||||
| 
 | ||||
|     <!-- Top components --> | ||||
|   <div class="pointer-events-none absolute top-0 left-0 w-full"> | ||||
| 
 | ||||
|     <div | ||||
|       id="top-bar" | ||||
|       class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap-reverse"> | ||||
|  | @ -412,7 +328,7 @@ | |||
|         <MapControlButton | ||||
|           cls="m-0.5 p-0.5 sm:p-1" | ||||
|           arialabel={Translations.t.general.labels.menu} | ||||
|           on:click={() => {state.guistate.menuIsOpened.setData(true)}} | ||||
|           on:click={() => {console.log("Opening...."); state.guistate.pageStates.menu.setData(true)}} | ||||
|           on:keydown={forwardEventToMap} | ||||
|         > | ||||
|           <MenuIcon class="h-6 w-6 cursor-pointer" /> | ||||
|  | @ -433,11 +349,15 @@ | |||
|         </MapControlButton> | ||||
|       </div> | ||||
| 
 | ||||
|       {#if $debug && $hash} | ||||
|         <div class="alert"> | ||||
|           {$hash} | ||||
|         </div> | ||||
|       {/if} | ||||
| 
 | ||||
|       <If condition={state.featureSwitches.featureSwitchSearch}> | ||||
|         <div class="w-full sm:w-80 md:w-96 my-2 sm:mt-0"> | ||||
|           <Geosearch {state} isFocused={state.searchState.searchIsFocused} | ||||
|                      searchContents={state.searchState.searchTerm} /> | ||||
|         <div class="w-full sm:w-64"> | ||||
|           <Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused}/> | ||||
|         </div> | ||||
|       </If> | ||||
| 
 | ||||
|  | @ -453,8 +373,9 @@ | |||
|       </If> | ||||
| 
 | ||||
|     </div> | ||||
|     <div class="float-left m-1 flex flex-col sm:mt-2"> | ||||
| 
 | ||||
|     <div class="float-left m-1 flex flex-col sm:mt-2"> | ||||
|       <!-- Current view tools --> | ||||
|       {#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()} | ||||
|         <MapControlButton | ||||
|           on:click={() => { | ||||
|  | @ -467,6 +388,7 @@ | |||
|           </div> | ||||
|         </MapControlButton> | ||||
|       {/if} | ||||
| 
 | ||||
|       <ExtraLinkButton {state} /> | ||||
|       <UploadingImageCounter featureId="*" showThankYou={false} {state} /> | ||||
|       <PendingChangesIndicator {state} /> | ||||
|  | @ -480,7 +402,8 @@ | |||
|         <div class="alert w-fit">Faking a user (Testmode)</div> | ||||
|       </If> | ||||
|     </div> | ||||
|     <div class="flex w-full flex-col items-center justify-center" > | ||||
| 
 | ||||
|     <div class="flex w-full flex-col items-center justify-center"> | ||||
|       <!-- Flex and w-full are needed for the positioning --> | ||||
|       <!-- Centermessage --> | ||||
|       <StateIndicator {state} /> | ||||
|  | @ -489,20 +412,6 @@ | |||
|   </div> | ||||
| 
 | ||||
| 
 | ||||
|   <LoginToggle ignoreLoading={true} {state}> | ||||
|     {#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $visualFeedback} | ||||
|       <!-- Don't use h-full: h-full does _not_ include the area under the URL-bar, which offsets the crosshair a bit --> | ||||
|       <div | ||||
|         class="pointer-events-none absolute top-0 left-0 flex w-full items-center justify-center" | ||||
|         style="height: 100vh" | ||||
|       > | ||||
|         <Cross class="h-4 w-4" /> | ||||
|       </div> | ||||
|     {/if} | ||||
|     <!-- Add in an empty container to remove error messages if login fails --> | ||||
|     <svelte:fragment slot="error" /> | ||||
|   </LoginToggle> | ||||
| 
 | ||||
|   <DrawerLeft shown={state.guistate.pageStates.menu}> | ||||
|     <div class="h-screen overflow-y-auto"> | ||||
|       <MenuDrawer onlyLink={true} {state} /> | ||||
|  |  | |||
							
								
								
									
										12
									
								
								src/Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								src/Utils.ts
									
										
									
									
									
								
							|  | @ -1808,6 +1808,18 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         return href | ||||
|     } | ||||
| 
 | ||||
|     /** Randomize array in-place using Durstenfeld shuffle algorithm | ||||
|      * Source: https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
 | ||||
|      * */ | ||||
|     static shuffle(array: any[]) { | ||||
|         for (let i = array.length - 1; i > 0; i--) { | ||||
|             const j = Math.floor(Math.random() * (i + 1)) | ||||
|             const temp = array[i] | ||||
|             array[i] = array[j] | ||||
|             array[j] = temp | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private static emojiRegex = /[\p{Extended_Pictographic}🛰️]/u | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue