forked from MapComplete/MapComplete
		
	Add themes to search functionality, including quickswitch between recent themes
This commit is contained in:
		
							parent
							
								
									b4866cdbac
								
							
						
					
					
						commit
						329865a15e
					
				
					 22 changed files with 679 additions and 431 deletions
				
			
		|  | @ -393,6 +393,9 @@ | ||||||
|         "search": { |         "search": { | ||||||
|             "error": "Something went wrong…", |             "error": "Something went wrong…", | ||||||
|             "nothing": "Nothing found…", |             "nothing": "Nothing found…", | ||||||
|  |             "nothingFor": "No results found for {term}", | ||||||
|  |             "recentThemes": "Recently visited maps", | ||||||
|  |             "recents": "Recent searches", | ||||||
|             "search": "Search a location", |             "search": "Search a location", | ||||||
|             "searchShort": "Search…", |             "searchShort": "Search…", | ||||||
|             "searching": "Searching…" |             "searching": "Searching…" | ||||||
|  |  | ||||||
|  | @ -2558,11 +2558,6 @@ video { | ||||||
|   border-color: rgb(209 213 219 / var(--tw-border-opacity)); |   border-color: rgb(209 213 219 / var(--tw-border-opacity)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .border-red-500 { |  | ||||||
|   --tw-border-opacity: 1; |  | ||||||
|   border-color: rgb(239 68 68 / var(--tw-border-opacity)); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .border-gray-800 { | .border-gray-800 { | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgb(31 41 55 / var(--tw-border-opacity)); |   border-color: rgb(31 41 55 / var(--tw-border-opacity)); | ||||||
|  | @ -2658,6 +2653,11 @@ video { | ||||||
|   border-color: rgb(34 197 94 / var(--tw-border-opacity)); |   border-color: rgb(34 197 94 / var(--tw-border-opacity)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .border-red-500 { | ||||||
|  |   --tw-border-opacity: 1; | ||||||
|  |   border-color: rgb(239 68 68 / var(--tw-border-opacity)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .border-gray-700 { | .border-gray-700 { | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgb(55 65 81 / var(--tw-border-opacity)); |   border-color: rgb(55 65 81 / var(--tw-border-opacity)); | ||||||
|  | @ -4379,10 +4379,10 @@ video { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| :root { | :root { | ||||||
|   /*  |   /* | ||||||
|     * The main colour scheme of mapcomplete is configured here. |       * The main colour scheme of mapcomplete is configured here. | ||||||
|     * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. |       * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. | ||||||
|     */ |       */ | ||||||
|   /* No support for dark mode yet, we disable it to prevent some elements to suddenly toggle */ |   /* No support for dark mode yet, we disable it to prevent some elements to suddenly toggle */ | ||||||
|   color-scheme: only light; |   color-scheme: only light; | ||||||
|   /* Main color of the application: the background and text colours */ |   /* Main color of the application: the background and text colours */ | ||||||
|  | @ -4407,9 +4407,9 @@ video { | ||||||
|   --disabled: #B8B8B8; |   --disabled: #B8B8B8; | ||||||
|   --disabled-font: #B8B8B8; |   --disabled-font: #B8B8B8; | ||||||
|   /** |   /** | ||||||
|      * Base colour of interactive elements, mainly the 'subtle button' |        * Base colour of interactive elements, mainly the 'subtle button' | ||||||
|      * @deprecated |        * @deprecated | ||||||
|      */ |        */ | ||||||
|   --subtle-detail-color: #dbeafe; |   --subtle-detail-color: #dbeafe; | ||||||
|   --subtle-detail-color-contrast: black; |   --subtle-detail-color-contrast: black; | ||||||
|   --subtle-detail-color-light-contrast: lightgrey; |   --subtle-detail-color-light-contrast: lightgrey; | ||||||
|  | @ -4419,14 +4419,14 @@ video { | ||||||
|   --catch-detail-color-contrast: #fb3afb; |   --catch-detail-color-contrast: #fb3afb; | ||||||
|   --image-carousel-height: 350px; |   --image-carousel-height: 350px; | ||||||
|   /** Technical value, used by icon.svelte |   /** Technical value, used by icon.svelte | ||||||
|      */ |        */ | ||||||
|   --svg-color: #000000; |   --svg-color: #000000; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @font-face{ | @font-face { | ||||||
|   font-family:"Source Sans Pro"; |   font-family: "Source Sans Pro"; | ||||||
| 
 | 
 | ||||||
|   src:url("/assets/source-sans-pro.regular.ttf") format("woff"); |   src: url("/assets/source-sans-pro.regular.ttf") format("woff"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /***********************************************************************\ | /***********************************************************************\ | ||||||
|  | @ -4663,18 +4663,18 @@ select:hover { | ||||||
| 
 | 
 | ||||||
| .neutral-label { | .neutral-label { | ||||||
|   /** This label styles as normal text. It's power comes from the many :not(.neutral-label) entries. |   /** This label styles as normal text. It's power comes from the many :not(.neutral-label) entries. | ||||||
|      * Placed here for autocompletion |        * Placed here for autocompletion | ||||||
|      */ |        */ | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| label:not(.neutral-label):not(.button) { | label:not(.neutral-label):not(.button) { | ||||||
|   /** |   /** | ||||||
|      * Label should _contain_ the input element |        * Label should _contain_ the input element | ||||||
|      */ |        */ | ||||||
|   padding: 0.25rem; |   padding: 0.25rem; | ||||||
|   padding-right: 0.5rem; |   padding-right: 0.5rem; | ||||||
|   padding-left: 0.5rem; |   padding-left: 0.5rem; | ||||||
|   margin:0.25rem; |   margin: 0.25rem; | ||||||
|   border-radius: 0.5rem; |   border-radius: 0.5rem; | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   box-sizing: border-box; |   box-sizing: border-box; | ||||||
|  | @ -4887,6 +4887,10 @@ a.link-underline { | ||||||
|   color: unset !important; |   color: unset !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | a:hover { | ||||||
|  |   background-color: var(--low-interaction-background); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .disable-links a.must-link, | .disable-links a.must-link, | ||||||
| .disable-links .must-link a { | .disable-links .must-link a { | ||||||
|   /* Hide links if they are disabled */ |   /* Hide links if they are disabled */ | ||||||
|  | @ -4901,7 +4905,7 @@ a.link-underline { | ||||||
| 
 | 
 | ||||||
| .selected svg:not(.noselect *) path.selectable { | .selected svg:not(.noselect *) path.selectable { | ||||||
|   /* A marker on the map gets the 'selected' class when it's properties are displayed |   /* A marker on the map gets the 'selected' class when it's properties are displayed | ||||||
|     */ |       */ | ||||||
|   stroke: white !important; |   stroke: white !important; | ||||||
|   stroke-width: 20px !important; |   stroke-width: 20px !important; | ||||||
|   overflow: visible !important; |   overflow: visible !important; | ||||||
|  | @ -4915,7 +4919,7 @@ a.link-underline { | ||||||
| 
 | 
 | ||||||
| .selected svg { | .selected svg { | ||||||
|   /* A marker on the map gets the 'selected' class when it's properties are displayed |   /* A marker on the map gets the 'selected' class when it's properties are displayed | ||||||
|     */ |       */ | ||||||
|   overflow: visible !important; |   overflow: visible !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,12 +1,13 @@ | ||||||
| import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
|  | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| export default class CombinedSearcher implements GeocodingProvider { | export default class CombinedSearcher implements GeocodingProvider { | ||||||
|     private _providers: ReadonlyArray<GeocodingProvider> |     private _providers: ReadonlyArray<GeocodingProvider> | ||||||
|     private _providersWithSuggest: ReadonlyArray<GeocodingProvider> |     private _providersWithSuggest: ReadonlyArray<GeocodingProvider> | ||||||
| 
 | 
 | ||||||
|     constructor(...providers: ReadonlyArray<GeocodingProvider>) { |     constructor(...providers: ReadonlyArray<GeocodingProvider>) { | ||||||
|         this._providers = providers |         this._providers = Utils.NoNull(providers) | ||||||
|         this._providersWithSuggest = providers.filter(pr => pr.suggest !== undefined) |         this._providersWithSuggest = this._providers.filter(pr => pr.suggest !== undefined) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -24,7 +24,7 @@ export type GeoCodeResult = { | ||||||
|     osm_type?: "node" | "way" | "relation" |     osm_type?: "node" | "way" | "relation" | ||||||
|     osm_id?: string, |     osm_id?: string, | ||||||
|     category?: GeocodingCategory, |     category?: GeocodingCategory, | ||||||
|     importance?: number |     payload?: object | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface GeocodingOptions { | export interface GeocodingOptions { | ||||||
|  |  | ||||||
|  | @ -7,21 +7,20 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| 
 | 
 | ||||||
| export class RecentSearch { | export class RecentSearch { | ||||||
| 
 | 
 | ||||||
|     private readonly _recentSearches: UIEventSource<string[]> |  | ||||||
|     public readonly recentSearches: Store<string[]> |  | ||||||
| 
 | 
 | ||||||
|     private readonly _seenThisSession: UIEventSource<GeoCodeResult[]> = new UIEventSource<GeoCodeResult[]>([]) |     private readonly _seenThisSession: UIEventSource<GeoCodeResult[]> | ||||||
|     public readonly seenThisSession: Store<GeoCodeResult[]> = this._seenThisSession |     public readonly seenThisSession: Store<GeoCodeResult[]> | ||||||
| 
 | 
 | ||||||
|     constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) { |     constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) { | ||||||
|         const longPref = state.osmConnection.preferencesHandler.GetLongPreference("recent-searches") |      //   const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches")
 | ||||||
|         this._recentSearches = longPref.sync(str => !str ? [] : <string[]>JSON.parse(str), [], strs => JSON.stringify(strs)) |         this._seenThisSession =  new UIEventSource<GeoCodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
 | ||||||
|         this.recentSearches = this._recentSearches |         this.seenThisSession = this._seenThisSession | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|         state.selectedElement.addCallbackAndRunD(selected => { |         state.selectedElement.addCallbackAndRunD(selected => { | ||||||
|             const [osm_type, osm_id] = selected.properties.id.split("/") |             const [osm_type, osm_id] = selected.properties.id.split("/") | ||||||
|             const [lon, lat] = GeoOperations.centerpointCoordinates(selected) |             const [lon, lat] = GeoOperations.centerpointCoordinates(selected) | ||||||
|             const entry = <GeoCodeResult> { |             const entry = <GeoCodeResult>{ | ||||||
|                 feature: selected, |                 feature: selected, | ||||||
|                 osm_id, osm_type, |                 osm_id, osm_type, | ||||||
|                 description: "Viewed recently", |                 description: "Viewed recently", | ||||||
|  | @ -33,7 +32,7 @@ export class RecentSearch { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addSelected(entry: GeoCodeResult) { |     addSelected(entry: GeoCodeResult) { | ||||||
|         const arr = [...this.seenThisSession.data.slice(0, 20), entry] |         const arr = [...(this.seenThisSession.data ?? []).slice(0, 20), entry] | ||||||
| 
 | 
 | ||||||
|         const seenIds = new Set<string>() |         const seenIds = new Set<string>() | ||||||
|         for (let i = arr.length - 1; i >= 0; i--) { |         for (let i = arr.length - 1; i >= 0; i--) { | ||||||
|  |  | ||||||
							
								
								
									
										43
									
								
								src/Logic/Geocoding/ThemeSearch.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/Logic/Geocoding/ThemeSearch.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | ||||||
|  | import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||||
|  | import * as themeOverview from "../../assets/generated/theme_overview.json" | ||||||
|  | import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||||
|  | import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||||
|  | import { Utils } from "../../Utils" | ||||||
|  | import MoreScreen from "../../UI/BigComponents/MoreScreen" | ||||||
|  | import { Store } from "../UIEventSource" | ||||||
|  | 
 | ||||||
|  | export default class ThemeSearch implements GeocodingProvider { | ||||||
|  | 
 | ||||||
|  |     private static allThemes: MinimalLayoutInformation[] = (themeOverview["default"] ?? themeOverview) | ||||||
|  |     private readonly _state: SpecialVisualizationState | ||||||
|  |     private readonly _knownHiddenThemes: Store<Set<string>> | ||||||
|  | 
 | ||||||
|  |     constructor(state: SpecialVisualizationState) { | ||||||
|  |         this._state = state | ||||||
|  |         this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||||
|  |         return this.suggest(query, options) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||||
|  |         if(query.length < 1){ | ||||||
|  |             return [] | ||||||
|  |         } | ||||||
|  |         const limit = options?.limit ?? 4 | ||||||
|  |         query = Utils.simplifyStringForSearch(query) | ||||||
|  |         const withMatch = ThemeSearch.allThemes | ||||||
|  |             .filter(th => !th.hideFromOverview ) | ||||||
|  |             .filter(th => th.id !== this._state.layout.id) | ||||||
|  |             .filter(th => MoreScreen.MatchesLayout(th, query)) | ||||||
|  |             .slice(0, limit + 1) | ||||||
|  | 
 | ||||||
|  |         return withMatch.map(match => (<GeoCodeResult> { | ||||||
|  |             payload: match, | ||||||
|  |             osm_id: match.id | ||||||
|  |         })) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -71,7 +71,7 @@ export class OsmPreferences { | ||||||
|             } |             } | ||||||
|             if (str === null) { |             if (str === null) { | ||||||
|                 console.error("Deleting " + allStartWith) |                 console.error("Deleting " + allStartWith) | ||||||
|                 let count = parseInt(length.data) |                 const count = parseInt(length.data) | ||||||
|                 for (let i = 0; i < count; i++) { |                 for (let i = 0; i < count; i++) { | ||||||
|                     // Delete all the preferences
 |                     // Delete all the preferences
 | ||||||
|                     self.GetPreference(allStartWith + "-" + i, "", subOptions).setData("") |                     self.GetPreference(allStartWith + "-" + i, "", subOptions).setData("") | ||||||
|  |  | ||||||
|  | @ -78,6 +78,10 @@ export default class UserRelatedState { | ||||||
|     public readonly preferencesAsTags: UIEventSource<Record<string, string>> |     public readonly preferencesAsTags: UIEventSource<Record<string, string>> | ||||||
|     private readonly _mapProperties: MapProperties |     private readonly _mapProperties: MapProperties | ||||||
| 
 | 
 | ||||||
|  |     private readonly _recentlyVisitedThemes: UIEventSource<string[]> | ||||||
|  |     public readonly recentlyVisitedThemes: Store<string[]> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         osmConnection: OsmConnection, |         osmConnection: OsmConnection, | ||||||
|         layout?: LayoutConfig, |         layout?: LayoutConfig, | ||||||
|  | @ -109,7 +113,7 @@ export default class UserRelatedState { | ||||||
|         this.showAllQuestionsAtOnce = UIEventSource.asBoolean( |         this.showAllQuestionsAtOnce = UIEventSource.asBoolean( | ||||||
|             this.osmConnection.GetPreference("show-all-questions", "false", { |             this.osmConnection.GetPreference("show-all-questions", "false", { | ||||||
|                 documentation: |                 documentation: | ||||||
|                     "Either 'true' or 'false'. If set, all questions will be shown all at once", |                     "Either 'true' or 'false'. If set, all questions will be shown all at once" | ||||||
|             }) |             }) | ||||||
|         ) |         ) | ||||||
|         this.language = this.osmConnection.GetPreference("language") |         this.language = this.osmConnection.GetPreference("language") | ||||||
|  | @ -129,7 +133,7 @@ export default class UserRelatedState { | ||||||
|             undefined, |             undefined, | ||||||
|             { |             { | ||||||
|                 documentation: |                 documentation: | ||||||
|                     "The ID of a layer or layer category that MapComplete uses by default", |                     "The ID of a layer or layer category that MapComplete uses by default" | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  | @ -137,12 +141,12 @@ export default class UserRelatedState { | ||||||
|             "preferences-add-new-mode", |             "preferences-add-new-mode", | ||||||
|             "button_click_right", |             "button_click_right", | ||||||
|             { |             { | ||||||
|                 documentation: "How adding a new feature is done", |                 documentation: "How adding a new feature is done" | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", { |         this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", { | ||||||
|             documentation: "The license under which new images are uploaded", |             documentation: "The license under which new images are uploaded" | ||||||
|         }) |         }) | ||||||
|         this.installedUserThemes = this.InitInstalledUserThemes() |         this.installedUserThemes = this.InitInstalledUserThemes() | ||||||
| 
 | 
 | ||||||
|  | @ -150,6 +154,30 @@ export default class UserRelatedState { | ||||||
| 
 | 
 | ||||||
|         this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches) |         this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches) | ||||||
| 
 | 
 | ||||||
|  |         const prefs = this.osmConnection | ||||||
|  |         this._recentlyVisitedThemes = UIEventSource.asObject(prefs.GetLongPreference("recently-visited-themes"), []) | ||||||
|  |         this.recentlyVisitedThemes = this._recentlyVisitedThemes | ||||||
|  |         if (layout) { | ||||||
|  |             const osmConn =this.osmConnection | ||||||
|  |             const recentlyVisited = this._recentlyVisitedThemes | ||||||
|  |             function update() { | ||||||
|  |                 if (!osmConn.isLoggedIn.data) { | ||||||
|  |                     return | ||||||
|  |                 } | ||||||
|  |                 const previously = recentlyVisited.data | ||||||
|  |                 if (previously[0] === layout.id) { | ||||||
|  |                     return true | ||||||
|  |                 } | ||||||
|  |                 const newThemes = Utils.Dedup([layout.id, ...previously]).slice(0, 30) | ||||||
|  |                 recentlyVisited.set(newThemes) | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             this._recentlyVisitedThemes.addCallbackAndRun(() => update()) | ||||||
|  |             this.osmConnection.isLoggedIn.addCallbackAndRun(() => update()) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         this.syncLanguage() |         this.syncLanguage() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -171,13 +199,13 @@ export default class UserRelatedState { | ||||||
| 
 | 
 | ||||||
|     public GetUnofficialTheme(id: string): |     public GetUnofficialTheme(id: string): | ||||||
|         | { |         | { | ||||||
|               id: string |         id: string | ||||||
|               icon: string |         icon: string | ||||||
|               title: any |         title: any | ||||||
|               shortDescription: any |         shortDescription: any | ||||||
|               definition?: any |         definition?: any | ||||||
|               isOfficial: boolean |         isOfficial: boolean | ||||||
|           } |     } | ||||||
|         | undefined { |         | undefined { | ||||||
|         console.log("GETTING UNOFFICIAL THEME") |         console.log("GETTING UNOFFICIAL THEME") | ||||||
|         const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) |         const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) | ||||||
|  | @ -202,8 +230,8 @@ export default class UserRelatedState { | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.warn( |             console.warn( | ||||||
|                 "Removing theme " + |                 "Removing theme " + | ||||||
|                     id + |                 id + | ||||||
|                     " as it could not be parsed from the preferences; the content is:", |                 " as it could not be parsed from the preferences; the content is:", | ||||||
|                 str |                 str | ||||||
|             ) |             ) | ||||||
|             pref.setData(null) |             pref.setData(null) | ||||||
|  | @ -233,7 +261,7 @@ export default class UserRelatedState { | ||||||
|                     icon: layout.icon, |                     icon: layout.icon, | ||||||
|                     title: layout.title.translations, |                     title: layout.title.translations, | ||||||
|                     shortDescription: layout.shortDescription.translations, |                     shortDescription: layout.shortDescription.translations, | ||||||
|                     definition: layout["definition"], |                     definition: layout["definition"] | ||||||
|                 }) |                 }) | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|  | @ -273,13 +301,13 @@ export default class UserRelatedState { | ||||||
|                         id: "home", |                         id: "home", | ||||||
|                         "user:home": "yes", |                         "user:home": "yes", | ||||||
|                         _lon: homeLonLat[0], |                         _lon: homeLonLat[0], | ||||||
|                         _lat: homeLonLat[1], |                         _lat: homeLonLat[1] | ||||||
|                     }, |                     }, | ||||||
|                     geometry: { |                     geometry: { | ||||||
|                         type: "Point", |                         type: "Point", | ||||||
|                         coordinates: homeLonLat, |                         coordinates: homeLonLat | ||||||
|                     }, |                     } | ||||||
|                 }, |                 } | ||||||
|             ] |             ] | ||||||
|         }) |         }) | ||||||
|         return new StaticFeatureSource(feature) |         return new StaticFeatureSource(feature) | ||||||
|  | @ -300,7 +328,7 @@ export default class UserRelatedState { | ||||||
|             _applicationOpened: new Date().toISOString(), |             _applicationOpened: new Date().toISOString(), | ||||||
|             _supports_sharing: |             _supports_sharing: | ||||||
|                 typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no", |                 typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no", | ||||||
|             _iframe: Utils.isIframe ? "yes" : "no", |             _iframe: Utils.isIframe ? "yes" : "no" | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         for (const key in Constants.userJourney) { |         for (const key in Constants.userJourney) { | ||||||
|  | @ -355,18 +383,18 @@ export default class UserRelatedState { | ||||||
|                     const zenLinks: { link: string; id: string }[] = Utils.NoNull([ |                     const zenLinks: { link: string; id: string }[] = Utils.NoNull([ | ||||||
|                         hasMissingTheme |                         hasMissingTheme | ||||||
|                             ? { |                             ? { | ||||||
|                                   id: "theme:" + layout.id, |                                 id: "theme:" + layout.id, | ||||||
|                                   link: LinkToWeblate.hrefToWeblateZen( |                                 link: LinkToWeblate.hrefToWeblateZen( | ||||||
|                                       language, |                                     language, | ||||||
|                                       "themes", |                                     "themes", | ||||||
|                                       layout.id |                                     layout.id | ||||||
|                                   ), |                                 ) | ||||||
|                               } |                             } | ||||||
|                             : undefined, |                             : undefined, | ||||||
|                         ...missingLayers.map((id) => ({ |                         ...missingLayers.map((id) => ({ | ||||||
|                             id: "layer:" + id, |                             id: "layer:" + id, | ||||||
|                             link: LinkToWeblate.hrefToWeblateZen(language, "layers", id), |                             link: LinkToWeblate.hrefToWeblateZen(language, "layers", id) | ||||||
|                         })), |                         })) | ||||||
|                     ]) |                     ]) | ||||||
|                     const untranslated_count = untranslated.length |                     const untranslated_count = untranslated.length | ||||||
|                     amendedPrefs.data["_translation_total"] = "" + total |                     amendedPrefs.data["_translation_total"] = "" + total | ||||||
|  | @ -391,8 +419,8 @@ export default class UserRelatedState { | ||||||
|             for (const k in userDetails) { |             for (const k in userDetails) { | ||||||
|                 amendedPrefs.data["_" + k] = "" + userDetails[k] |                 amendedPrefs.data["_" + k] = "" + userDetails[k] | ||||||
|             } |             } | ||||||
|             if(userDetails.description){ |             if (userDetails.description) { | ||||||
|                 amendedPrefs.data["_description_html"] =  Utils.purify(new Showdown.Converter() |                 amendedPrefs.data["_description_html"] = Utils.purify(new Showdown.Converter() | ||||||
|                     .makeHtml(userDetails.description) |                     .makeHtml(userDetails.description) | ||||||
|                     ?.replace(/>/g, ">") |                     ?.replace(/>/g, ">") | ||||||
|                     ?.replace(/</g, "<") |                     ?.replace(/</g, "<") | ||||||
|  |  | ||||||
|  | @ -104,7 +104,9 @@ export abstract class Store<T> implements Readable<T> { | ||||||
|         extraStoresToWatch: Store<any>[], |         extraStoresToWatch: Store<any>[], | ||||||
|         callbackDestroyFunction: (f: () => void) => void |         callbackDestroyFunction: (f: () => void) => void | ||||||
|     ): Store<J> |     ): Store<J> | ||||||
|  | 
 | ||||||
|     M |     M | ||||||
|  | 
 | ||||||
|     public mapD<J>( |     public mapD<J>( | ||||||
|         f: (t: Exclude<T, undefined | null>) => J, |         f: (t: Exclude<T, undefined | null>) => J, | ||||||
|         extraStoresToWatch?: Store<any>[], |         extraStoresToWatch?: Store<any>[], | ||||||
|  | @ -246,6 +248,7 @@ export abstract class Store<T> implements Readable<T> { | ||||||
|             return f(<Exclude<T, undefined | null>>t) |             return f(<Exclude<T, undefined | null>>t) | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     public stabilized(millisToStabilize): Store<T> { |     public stabilized(millisToStabilize): Store<T> { | ||||||
|         if (Utils.runningFromConsole) { |         if (Utils.runningFromConsole) { | ||||||
|             return this |             return this | ||||||
|  | @ -311,12 +314,14 @@ export class ImmutableStore<T> extends Store<T> { | ||||||
|     public readonly data: T |     public readonly data: T | ||||||
|     static FALSE = new ImmutableStore<boolean>(false) |     static FALSE = new ImmutableStore<boolean>(false) | ||||||
|     static TRUE = new ImmutableStore<boolean>(true) |     static TRUE = new ImmutableStore<boolean>(true) | ||||||
|  | 
 | ||||||
|     constructor(data: T) { |     constructor(data: T) { | ||||||
|         super() |         super() | ||||||
|         this.data = data |         this.data = data | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static readonly pass: () => void = () => {} |     private static readonly pass: () => void = () => { | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     addCallback(_: (data: T) => void): () => void { |     addCallback(_: (data: T) => void): () => void { | ||||||
|         // pass: data will never change
 |         // pass: data will never change
 | ||||||
|  | @ -718,6 +723,27 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     static asObject<T extends object>(stringUIEventSource: UIEventSource<string>, defaultV: T): UIEventSource<T> { | ||||||
|  |         return stringUIEventSource.sync( | ||||||
|  |             (str) => { | ||||||
|  |                 if (str === undefined || str === null || str === "") { | ||||||
|  |                     return defaultV | ||||||
|  |                 } | ||||||
|  |                 try { | ||||||
|  |                     return <T> JSON.parse(str) | ||||||
|  |                 } catch (e) { | ||||||
|  |                     console.error("Could not parse value", str,"due to",e) | ||||||
|  |                     return defaultV | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             [], | ||||||
|  |             (b) => { | ||||||
|  |                 console.log("Stringifying", b) | ||||||
|  |                 return JSON.stringify(b) ?? "" | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well. |      * Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well. | ||||||
|      * However, this value can be overriden without affecting source |      * However, this value can be overriden without affecting source | ||||||
|  | @ -863,7 +889,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | ||||||
| 
 | 
 | ||||||
|         const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee) |         const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee) | ||||||
| 
 | 
 | ||||||
|         const update = function () { |         const update = function() { | ||||||
|             newSource.setData(f(self.data)) |             newSource.setData(f(self.data)) | ||||||
|             return allowUnregister && newSource._callbacks.length() === 0 |             return allowUnregister && newSource._callbacks.length() === 0 | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -12,7 +12,21 @@ import { RasterLayerProperties } from "../RasterLayerProperties" | ||||||
| 
 | 
 | ||||||
| import { ConversionContext } from "./Conversion/ConversionContext" | import { ConversionContext } from "./Conversion/ConversionContext" | ||||||
| import { Translatable } from "./Json/Translatable" | import { Translatable } from "./Json/Translatable" | ||||||
|  | import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Minimal information about a theme | ||||||
|  |  **/ | ||||||
|  | export class MinimalLayoutInformation { | ||||||
|  |     id: string | ||||||
|  |     icon: string | ||||||
|  |     title: Translatable | ||||||
|  |     shortDescription: Translatable | ||||||
|  |     definition?: Translatable | ||||||
|  |     mustHaveLanguage?: boolean | ||||||
|  |     hideFromOverview?: boolean | ||||||
|  |     keywords?: (Translatable | TagRenderingConfigJson)[] | ||||||
|  | } | ||||||
| /** | /** | ||||||
|  * Minimal information about a theme |  * Minimal information about a theme | ||||||
|  **/ |  **/ | ||||||
|  | @ -27,6 +41,8 @@ export class LayoutInformation { | ||||||
|     keywords?: (Translatable | Translation)[] |     keywords?: (Translatable | Translation)[] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| export default class LayoutConfig implements LayoutInformation { | export default class LayoutConfig implements LayoutInformation { | ||||||
|     public static readonly defaultSocialImage = "assets/SocialImage.png" |     public static readonly defaultSocialImage = "assets/SocialImage.png" | ||||||
|     public readonly id: string |     public readonly id: string | ||||||
|  |  | ||||||
|  | @ -81,6 +81,7 @@ import CoordinateSearch from "../Logic/Geocoding/CoordinateSearch" | ||||||
| import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch" | import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch" | ||||||
| import { RecentSearch } from "../Logic/Geocoding/RecentSearch" | import { RecentSearch } from "../Logic/Geocoding/RecentSearch" | ||||||
| import PhotonSearch from "../Logic/Geocoding/PhotonSearch" | import PhotonSearch from "../Logic/Geocoding/PhotonSearch" | ||||||
|  | import ThemeSearch from "../Logic/Geocoding/ThemeSearch" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * |  * | ||||||
|  | @ -393,6 +394,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             new LocalElementSearch(this, 5), |             new LocalElementSearch(this, 5), | ||||||
|             new PhotonSearch(), // new NominatimGeocoding(),
 |             new PhotonSearch(), // new NominatimGeocoding(),
 | ||||||
|             new CoordinateSearch(), |             new CoordinateSearch(), | ||||||
|  |             this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this) : undefined | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         this.recentlySearched = new RecentSearch(this) |         this.recentlySearched = new RecentSearch(this) | ||||||
|  |  | ||||||
|  | @ -35,7 +35,7 @@ | ||||||
|       "oauth_token", |       "oauth_token", | ||||||
|       undefined, |       undefined, | ||||||
|       "Used to complete the login" |       "Used to complete the login" | ||||||
|     ), |     ) | ||||||
|   }) |   }) | ||||||
|   const state = new UserRelatedState(osmConnection) |   const state = new UserRelatedState(osmConnection) | ||||||
|   const t = Translations.t.index |   const t = Translations.t.index | ||||||
|  | @ -44,7 +44,7 @@ | ||||||
|   let userLanguages = osmConnection.userDetails.map((ud) => ud.languages) |   let userLanguages = osmConnection.userDetails.map((ud) => ud.languages) | ||||||
|   let themeSearchText: UIEventSource<string | undefined> = new UIEventSource<string>(undefined) |   let themeSearchText: UIEventSource<string | undefined> = new UIEventSource<string>(undefined) | ||||||
| 
 | 
 | ||||||
|   document.addEventListener("keydown", function (event) { |   document.addEventListener("keydown", function(event) { | ||||||
|     if (event.ctrlKey && event.code === "KeyF") { |     if (event.ctrlKey && event.code === "KeyF") { | ||||||
|       document.getElementById("theme-search")?.focus() |       document.getElementById("theme-search")?.focus() | ||||||
|       event.preventDefault() |       event.preventDefault() | ||||||
|  | @ -55,20 +55,10 @@ | ||||||
|   const hiddenThemes: LayoutInformation[] = |   const hiddenThemes: LayoutInformation[] = | ||||||
|     (themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? [] |     (themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? [] | ||||||
|   { |   { | ||||||
|     const prefix = "mapcomplete-hidden-theme-" |     visitedHiddenThemes = MoreScreen.knownHiddenThemes(state.osmConnection) | ||||||
|     const userPreferences = state.osmConnection.preferencesHandler.preferences |       .map((knownIds) => hiddenThemes.filter((theme) => | ||||||
|     visitedHiddenThemes = userPreferences.map((preferences) => { |         knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" | ||||||
|       const knownIds = new Set<string>( |       )) | ||||||
|         Object.keys(preferences) |  | ||||||
|           .filter((key) => key.startsWith(prefix)) |  | ||||||
|           .map((key) => key.substring(prefix.length, key.length - "-enabled".length)) |  | ||||||
|       ) |  | ||||||
|       return hiddenThemes.filter( |  | ||||||
|         (theme) => |  | ||||||
|           knownIds.has(theme.id) || |  | ||||||
|           state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" |  | ||||||
|       ) |  | ||||||
|     }) |  | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -103,7 +93,7 @@ | ||||||
| 
 | 
 | ||||||
|     <form |     <form | ||||||
|       class="flex justify-center" |       class="flex justify-center" | ||||||
|       on:submit|preventDefault={(_) => MoreScreen.applySearch(themeSearchText.data)} |       on:submit|preventDefault={() => MoreScreen.applySearch(themeSearchText.data)} | ||||||
|     > |     > | ||||||
|       <label |       <label | ||||||
|         class="neutral-label my-2 flex w-full items-center rounded-full border-2 border-black sm:w-1/2" |         class="neutral-label my-2 flex w-full items-center rounded-full border-2 border-black sm:w-1/2" | ||||||
|  |  | ||||||
|  | @ -116,8 +116,8 @@ | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   let suggestions: Store<GeoCodeResult[]> = searchContents.stabilized(250).bindD(search => |   let suggestions: Store<{success: GeoCodeResult[]} | {error}> = searchContents.stabilized(250).bindD(search => | ||||||
|     UIEventSource.FromPromise(searcher.suggest(search), err => console.error(err)) |     UIEventSource.FromPromiseWithErr(searcher.suggest(search)) | ||||||
|   ) |   ) | ||||||
| 
 | 
 | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,19 +1,27 @@ | ||||||
| import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import { ImmutableStore, Store } from "../../Logic/UIEventSource" | import { Store } from "../../Logic/UIEventSource" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import themeOverview from "../../assets/generated/theme_overview.json" | import themeOverview from "../../assets/generated/theme_overview.json" | ||||||
| import Locale from "../i18n/Locale" | import Locale from "../i18n/Locale" | ||||||
|  | import { Translatable } from "../../Models/ThemeConfig/Json/Translatable" | ||||||
|  | import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson" | ||||||
|  | import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||||
| 
 | 
 | ||||||
| export default class MoreScreen { | export default class MoreScreen { | ||||||
|     public static readonly officialThemes: LayoutInformation[] = themeOverview |     public static readonly officialThemes: MinimalLayoutInformation[] = themeOverview | ||||||
| 
 |     public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>() | ||||||
|  |     static { | ||||||
|  |         for (const th of MoreScreen.officialThemes) { | ||||||
|  |             MoreScreen.officialThemesById.set(th.id, th) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|     public static applySearch(searchTerm: string) { |     public static applySearch(searchTerm: string) { | ||||||
|         searchTerm = searchTerm.toLowerCase() |         searchTerm = searchTerm.toLowerCase() | ||||||
|         if (!searchTerm) { |         if (!searchTerm) { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         if (searchTerm === "personal") { |         if (searchTerm === "personal") { | ||||||
|             window.location.href = MoreScreen.createUrlFor({ id: "personal" }, false).data |             window.location.href = MoreScreen.createUrlFor({ id: "personal" }, false) | ||||||
|         } |         } | ||||||
|         if (searchTerm === "bugs" || searchTerm === "issues") { |         if (searchTerm === "bugs" || searchTerm === "issues") { | ||||||
|             window.location.href = "https://github.com/pietervdvn/MapComplete/issues" |             window.location.href = "https://github.com/pietervdvn/MapComplete/issues" | ||||||
|  | @ -38,22 +46,22 @@ export default class MoreScreen { | ||||||
|                 MoreScreen.MatchesLayout(th, searchTerm) |                 MoreScreen.MatchesLayout(th, searchTerm) | ||||||
|         ) |         ) | ||||||
|         if (publicTheme !== undefined) { |         if (publicTheme !== undefined) { | ||||||
|             window.location.href = MoreScreen.createUrlFor(publicTheme, false).data |             window.location.href = MoreScreen.createUrlFor(publicTheme, false) | ||||||
|         } |         } | ||||||
|         const hiddenTheme = MoreScreen.officialThemes.find( |         const hiddenTheme = MoreScreen.officialThemes.find( | ||||||
|             (th) => th.id !== "personal" && MoreScreen.MatchesLayout(th, searchTerm) |             (th) => th.id !== "personal" && MoreScreen.MatchesLayout(th, searchTerm) | ||||||
|         ) |         ) | ||||||
|         if (hiddenTheme !== undefined) { |         if (hiddenTheme !== undefined) { | ||||||
|             window.location.href = MoreScreen.createUrlFor(hiddenTheme, false).data |             window.location.href = MoreScreen.createUrlFor(hiddenTheme, false) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static MatchesLayout( |     public static MatchesLayout( | ||||||
|         layout: { |         layout: { | ||||||
|             id: string |             id: string | ||||||
|             title: any |             title: Translatable | ||||||
|             shortDescription: any |             shortDescription: Translatable | ||||||
|             keywords?: any[] |             keywords?: (Translatable | TagRenderingConfigJson)[] | ||||||
|         }, |         }, | ||||||
|         search: string |         search: string | ||||||
|     ): boolean { |     ): boolean { | ||||||
|  | @ -72,7 +80,7 @@ export default class MoreScreen { | ||||||
|             if (entity === undefined) { |             if (entity === undefined) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             const term = entity["*"] ?? entity[Locale.language.data] |             const term: string = entity["*"] ?? entity[Locale.language.data] | ||||||
|             if (Utils.RemoveDiacritics(term?.toLowerCase())?.indexOf(search) >= 0) { |             if (Utils.RemoveDiacritics(term?.toLowerCase())?.indexOf(search) >= 0) { | ||||||
|                 return true |                 return true | ||||||
|             } |             } | ||||||
|  | @ -82,10 +90,10 @@ export default class MoreScreen { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static createUrlFor( |     public static createUrlFor( | ||||||
|         layout: { id: string; definition?: string }, |         layout: { id: string }, | ||||||
|         isCustom: boolean, |         isCustom: boolean, | ||||||
|         state?: { layoutToUse?: { id } } |         state?: { layoutToUse?: { id } } | ||||||
|     ): Store<string> { |     ): string { | ||||||
|         if (layout === undefined) { |         if (layout === undefined) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|  | @ -115,11 +123,22 @@ export default class MoreScreen { | ||||||
|             linkPrefix = `${path}/theme.html?userlayout=${layout.id}&` |             linkPrefix = `${path}/theme.html?userlayout=${layout.id}&` | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let hash = "" |  | ||||||
|         if (layout.definition !== undefined) { |  | ||||||
|             hash = "#" + btoa(JSON.stringify(layout.definition)) |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         return new ImmutableStore<string>(`${linkPrefix}${hash}`) |         return `${linkPrefix}` | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gives all the IDs of the hidden themes which were previously visited | ||||||
|  |      * @param osmConnection | ||||||
|  |      */ | ||||||
|  |     public static knownHiddenThemes(osmConnection: OsmConnection): Store<Set<string>> { | ||||||
|  |         const prefix = "mapcomplete-hidden-theme-" | ||||||
|  |         const userPreferences = osmConnection.preferencesHandler.preferences | ||||||
|  |         return userPreferences.map((preferences) => | ||||||
|  |             new Set<string>( | ||||||
|  |                 Object.keys(preferences) | ||||||
|  |                     .filter((key) => key.startsWith(prefix)) | ||||||
|  |                     .map((key) => key.substring(prefix.length, key.length - "-enabled".length)) | ||||||
|  |             )) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -12,11 +12,15 @@ | ||||||
|   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" |   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" | ||||||
|   import { UIEventSource } from "../../Logic/UIEventSource" |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|   import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp" |   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" | ||||||
| 
 | 
 | ||||||
|   export let entry: GeoCodeResult |   export let entry: GeoCodeResult | ||||||
|   export let state: SpecialVisualizationState |   export let state: SpecialVisualizationState | ||||||
|   let layer: LayerConfig |   let layer: LayerConfig | ||||||
|   let tags : UIEventSource<Record<string, string>> |   let tags: UIEventSource<Record<string, string>> | ||||||
|   if (entry.feature?.properties?.id) { |   if (entry.feature?.properties?.id) { | ||||||
|     layer = state.layout.getMatchingLayer(entry.feature.properties) |     layer = state.layout.getMatchingLayer(entry.feature.properties) | ||||||
|     tags = state.featureProperties.getStore(entry.feature.properties.id) |     tags = state.featureProperties.getStore(entry.feature.properties.id) | ||||||
|  | @ -28,6 +32,8 @@ | ||||||
|   let mapRotation = state.mapProperties.rotation |   let mapRotation = state.mapProperties.rotation | ||||||
|   let inView = state.mapProperties.bounds.mapD(bounds => bounds.contains([entry.lon, entry.lat])) |   let inView = state.mapProperties.bounds.mapD(bounds => bounds.contains([entry.lon, entry.lat])) | ||||||
| 
 | 
 | ||||||
|  |   let otherTheme: MinimalLayoutInformation | undefined = <MinimalLayoutInformation>entry.payload | ||||||
|  | 
 | ||||||
|   function select() { |   function select() { | ||||||
|     console.log("Selected search entry", entry) |     console.log("Selected search entry", entry) | ||||||
|     if (entry.boundingbox) { |     if (entry.boundingbox) { | ||||||
|  | @ -41,44 +47,60 @@ | ||||||
|     } else { |     } else { | ||||||
|       state.mapProperties.flyTo(entry.lon, entry.lat, GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17) |       state.mapProperties.flyTo(entry.lon, entry.lat, GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17) | ||||||
|     } |     } | ||||||
|     if (entry.feature) { |     if (entry.feature?.properties?.id) { | ||||||
|       state.selectedElement.set(entry.feature) |       state.selectedElement.set(entry.feature) | ||||||
|     } |     } | ||||||
|     state.recentlySearched.addSelected(entry) |     state.recentlySearched.addSelected(entry) | ||||||
|     dispatch("select") |     dispatch("select") | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
| <button class="unstyled w-full link-no-underline" on:click={() => select() }> |  | ||||||
|   <div class="p-2 flex items-center w-full gap-y-2 w-full"> |  | ||||||
| 
 | 
 | ||||||
|     {#if layer} | {#if otherTheme} | ||||||
|       <ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} /> |   <a href={ MoreScreen.createUrlFor(otherTheme, false)} class="flex items-center p-2 w-full gap-y-2 rounded-xl" > | ||||||
|     {:else if entry.category} | 
 | ||||||
|       <Icon icon={GeocodingUtils.categoryToIcon[entry.category]} clss="w-6 h-6 shrink-0" color="#aaa" /> |     <Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" /> | ||||||
|     {/if} |     <div class="flex flex-col"> | ||||||
|     <div class="flex flex-col items-start pl-2 w-full"> |       <b> | ||||||
|       <div class="flex flex-wrap gap-x-2 justify-between w-full"> |         <Tr t={new Translation(otherTheme.title)} /> | ||||||
|         <b class="nowrap"> |       </b> | ||||||
|           {#if layer && $tags?.id} |       <!--<Tr t={new Translation(otherTheme.shortDescription)} /> --> | ||||||
|             <TagRenderingAnswer config={layer.title} selectedElement={entry.feature} {state} {tags} {layer} /> |  | ||||||
|           {:else} |  | ||||||
|             {entry.display_name ?? entry.osm_id} |  | ||||||
|           {/if} |  | ||||||
|         </b> |  | ||||||
|         <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> |  | ||||||
|       </div> |  | ||||||
|       {#if entry.description} |  | ||||||
|         <div class="subtle flex justify-between w-full"> |  | ||||||
|           {entry.description} |  | ||||||
|         </div> |  | ||||||
|       {/if} |  | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </a> | ||||||
| </button> | 
 | ||||||
|  | {:else} | ||||||
|  |   <button class="unstyled w-full link-no-underline" on:click={() => select() }> | ||||||
|  |     <div class="p-2 flex items-center w-full gap-y-2"> | ||||||
|  |       {#if layer} | ||||||
|  |         <ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} /> | ||||||
|  |       {: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> | ||||||
|  |           <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> | ||||||
|  |         </div> | ||||||
|  |         {#if entry.description} | ||||||
|  |           <div class="subtle flex justify-between w-full"> | ||||||
|  |             {entry.description} | ||||||
|  |           </div> | ||||||
|  |         {/if} | ||||||
|  | 
 | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </button> | ||||||
|  | 
 | ||||||
|  | {/if} | ||||||
|  |  | ||||||
|  | @ -3,59 +3,81 @@ | ||||||
|   import SearchResult from "./SearchResult.svelte" |   import SearchResult from "./SearchResult.svelte" | ||||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|   import { XMarkIcon } from "@babeard/svelte-heroicons/solid" |   import { XMarkIcon } from "@babeard/svelte-heroicons/solid" | ||||||
|   import { Store } from "../../Logic/UIEventSource" |   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
|   import Loading from "../Base/Loading.svelte" |   import Loading from "../Base/Loading.svelte" | ||||||
|  |   import Tr from "../Base/Tr.svelte" | ||||||
|  |   import Translations from "../i18n/Translations" | ||||||
|  |   import MoreScreen from "./MoreScreen" | ||||||
| 
 | 
 | ||||||
|   export let state: SpecialVisualizationState |   export let state: SpecialVisualizationState | ||||||
|   export let results: GeoCodeResult[] |   export let results: { success: GeoCodeResult[] } | { error } | ||||||
|   export let searchTerm: Store<string> |   export let searchTerm: Store<string> | ||||||
|   export let isFocused: Store<boolean> |   export let isFocused: UIEventSource<boolean> | ||||||
| 
 | 
 | ||||||
|   let recentlySeen: Store<GeoCodeResult[]> = state.recentlySearched.seenThisSession |   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> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="w-full collapsable" style="height: 50rem;" class:collapsed={!$isFocused}> | <div class="w-full collapsable" style="height: 50rem;" class:collapsed={!$isFocused}> | ||||||
|   {#if $searchTerm.length > 0 && results === undefined} |   {#if results?.["error"] !== undefined} | ||||||
|  |     <div class="searchbox normal-background items-center"> | ||||||
|  |       An error occured | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |   {:else if $searchTerm.length > 0 && results === undefined} | ||||||
|     <div class="searchbox normal-background items-center"> |     <div class="searchbox normal-background items-center"> | ||||||
|       <Loading /> |       <Loading /> | ||||||
|     </div> |     </div> | ||||||
|   {:else if results?.length > 0} |   {:else if results?.["success"]?.length > 0} | ||||||
|     <div class="relative w-full h-full"> |     <div class="relative w-full h-full"> | ||||||
|       <div class="absolute top-0 right-0 searchbox normal-background" |       <div class="absolute top-0 right-0 searchbox normal-background" | ||||||
|            style="width: 25rem"> |            style="width: 25rem"> | ||||||
|         <div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto"> |         <div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto"> | ||||||
| 
 | 
 | ||||||
|           {#each results as entry (entry)} |           {#each results["success"] as entry (entry)} | ||||||
|             <SearchResult on:select {entry} {state} /> |             <SearchResult on:select {entry} {state} /> | ||||||
|           {/each} |           {/each} | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|       <div class="absolute top-2 right-2 cursor-pointer" on:click={() => close()}> |       <div class="absolute top-2 right-2 cursor-pointer" on:click={() => isFocused.setData(false)}> | ||||||
|         <XMarkIcon class="w-4 h-4 hover:bg-stone-200 rounded-full" /> |         <XMarkIcon class="w-4 h-4 hover:bg-stone-200 rounded-full" /> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   {:else } |   {:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0} | ||||||
| 
 | 
 | ||||||
|       <div class="searchbox normal-background "> |     <div class="searchbox normal-background overflow-y-auto h-full"> | ||||||
|     {#if $searchTerm.length > 0} |       {#if $searchTerm.length > 0} | ||||||
|       <!-- TODO add translation --> |         <b class="flex justify-center p-4"> | ||||||
|         <b class="flex justify-center p-4">No results found for {$searchTerm}</b> |           <Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} /> | ||||||
|     {/if} |         </b> | ||||||
|  |       {/if} | ||||||
| 
 | 
 | ||||||
|     {#if $recentlySeen?.length > 0} |       {#if $recentlySeen?.length > 0} | ||||||
|       <!-- TODO add translation --> |         <h3 class="mx-2"> | ||||||
|         <h4>Recent searches</h4> |           <Tr t={Translations.t.general.search.recents} /> | ||||||
|  |         </h3> | ||||||
|         {#each $recentlySeen as entry} |         {#each $recentlySeen as entry} | ||||||
|           <SearchResult {entry} {state} on:select /> |           <SearchResult {entry} {state} on:select /> | ||||||
|         {/each} |         {/each} | ||||||
|     {/if} |       {/if} | ||||||
|       </div> | 
 | ||||||
|  |       {#if $recentThemes?.length > 0 && $allowOtherThemes} | ||||||
|  |         <h3 class="mx-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} | ||||||
|  |       {/if} | ||||||
|  |     </div> | ||||||
|   {/if} |   {/if} | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| <style> | <style> | ||||||
|     .searchbox { |     .searchbox { | ||||||
|         display: flex; |         display: flex; | ||||||
|  | @ -68,7 +90,8 @@ | ||||||
| 
 | 
 | ||||||
|     .collapsable { |     .collapsable { | ||||||
|         max-height: 50vh; |         max-height: 50vh; | ||||||
|         transition: max-height 350ms ease-in-out; |         transition: max-height 400ms linear; | ||||||
|  |         transition-delay: 500ms; | ||||||
|         overflow: hidden; |         overflow: hidden; | ||||||
|         padding: 0 !important; |         padding: 0 !important; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { Translation } from "../i18n/Translation" |  | ||||||
|   import * as personal from "../../../assets/themes/personal/personal.json" |   import * as personal from "../../../assets/themes/personal/personal.json" | ||||||
|   import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" |   import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
|   import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection" |   import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||||
|  |  | ||||||
|  | @ -23,15 +23,16 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|         "dragRotate", |         "dragRotate", | ||||||
|         "dragPan", |         "dragPan", | ||||||
|         "keyboard", |         "keyboard", | ||||||
|         "touchZoomRotate", |         "touchZoomRotate" | ||||||
|     ] |     ] | ||||||
|     private static maplibre_zoom_handlers = [ |     private static maplibre_zoom_handlers = [ | ||||||
|         "scrollZoom", |         "scrollZoom", | ||||||
|         "boxZoom", |         "boxZoom", | ||||||
|         "doubleClickZoom", |         "doubleClickZoom", | ||||||
|         "touchZoomRotate", |         "touchZoomRotate" | ||||||
|     ] |     ] | ||||||
|     readonly location: UIEventSource<{ lon: number; lat: number }> |     readonly location: UIEventSource<{ lon: number; lat: number }> | ||||||
|  |     private readonly isFlying = new UIEventSource(false) | ||||||
|     readonly zoom: UIEventSource<number> |     readonly zoom: UIEventSource<number> | ||||||
|     readonly bounds: UIEventSource<BBox> |     readonly bounds: UIEventSource<BBox> | ||||||
|     readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> |     readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> | ||||||
|  | @ -105,6 +106,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|         new RasterLayerHandler(this._maplibreMap, this.rasterLayer) |         new RasterLayerHandler(this._maplibreMap, this.rasterLayer) | ||||||
| 
 | 
 | ||||||
|         const clickmodes = ["left", "middle", "right"] as const |         const clickmodes = ["left", "middle", "right"] as const | ||||||
|  | 
 | ||||||
|         function handleClick(e: maplibregl.MapMouseEvent, mode?: "left" | "right" | "middle") { |         function handleClick(e: maplibregl.MapMouseEvent, mode?: "left" | "right" | "middle") { | ||||||
|             if (e.originalEvent["consumed"]) { |             if (e.originalEvent["consumed"]) { | ||||||
|                 // Workaround, 'ShowPointLayer' sets this flag
 |                 // Workaround, 'ShowPointLayer' sets this flag
 | ||||||
|  | @ -144,7 +146,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|             self.SetRotation(self.rotation.data) |             self.SetRotation(self.rotation.data) | ||||||
|             self.setTerrain(self.useTerrain.data) |             self.setTerrain(self.useTerrain.data) | ||||||
|             this.updateStores(true) |             this.updateStores(true) | ||||||
|             map.on("moveend", () => this.updateStores()) |             map.on("movestart", () => { | ||||||
|  |                 this.isFlying.setData(true) | ||||||
|  |             }) | ||||||
|  |             map.on("moveend", () => { | ||||||
|  |                 this.isFlying.setData(false) | ||||||
|  |                 this.updateStores() | ||||||
|  |             }) | ||||||
|             map.on("click", (e) => { |             map.on("click", (e) => { | ||||||
|                 handleClick(e) |                 handleClick(e) | ||||||
|             }) |             }) | ||||||
|  | @ -228,9 +236,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|         return { |         return { | ||||||
|             map: mlmap, |             map: mlmap, | ||||||
|             ui: new SvelteUIElement(MaplibreMap, { |             ui: new SvelteUIElement(MaplibreMap, { | ||||||
|                 map: mlmap, |                 map: mlmap | ||||||
|             }), |             }), | ||||||
|             mapproperties: new MapLibreAdaptor(mlmap), |             mapproperties: new MapLibreAdaptor(mlmap) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -298,7 +306,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|     ) { |     ) { | ||||||
|         const event = { |         const event = { | ||||||
|             date: new Date(), |             date: new Date(), | ||||||
|             key: key, |             key: key | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         for (let i = 0; i < this._onKeyNavigation.length; i++) { |         for (let i = 0; i < this._onKeyNavigation.length; i++) { | ||||||
|  | @ -487,7 +495,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|         const bounds = map.getBounds() |         const bounds = map.getBounds() | ||||||
|         const bbox = new BBox([ |         const bbox = new BBox([ | ||||||
|             [bounds.getEast(), bounds.getNorth()], |             [bounds.getEast(), bounds.getNorth()], | ||||||
|             [bounds.getWest(), bounds.getSouth()], |             [bounds.getWest(), bounds.getSouth()] | ||||||
|         ]) |         ]) | ||||||
|         if (this.bounds.data === undefined || !isSetup) { |         if (this.bounds.data === undefined || !isSetup) { | ||||||
|             this.bounds.setData(bbox) |             this.bounds.setData(bbox) | ||||||
|  | @ -501,6 +509,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|         if (!map || z === undefined) { |         if (!map || z === undefined) { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|  |         if (this.isFlying.data) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|         if (Math.abs(map.getZoom() - z) > 0.01) { |         if (Math.abs(map.getZoom() - z) > 0.01) { | ||||||
|             map.setZoom(z) |             map.setZoom(z) | ||||||
|         } |         } | ||||||
|  | @ -648,9 +659,22 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|         if (!hasDiff) { |         if (!hasDiff) { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|  |         this.lockZoom() | ||||||
|         map.fitBounds(bounds.toLngLat()) |         map.fitBounds(bounds.toLngLat()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Should be called before making an animation. | ||||||
|  |      * First, 'isFlying' is set to true. This will disable the zoom control | ||||||
|  |      * Then, zoom is set to '1', which is very low. This will generally disable all layers, after which this function will return | ||||||
|  |      * | ||||||
|  |      * Then, a zoom/pan/... animation can be made; after which a 'moveEnd'-event will trigger the 'isFlying' to be set to false and the zoom to be set correctly | ||||||
|  |      */ | ||||||
|  |     private lockZoom() { | ||||||
|  |         this.isFlying.setData(true) | ||||||
|  |         this.zoom.setData(1) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private async setTerrain(useTerrain: boolean) { |     private async setTerrain(useTerrain: boolean) { | ||||||
|         const map = this._maplibreMap.data |         const map = this._maplibreMap.data | ||||||
|         if (!map) { |         if (!map) { | ||||||
|  | @ -665,14 +689,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|                 type: "raster-dem", |                 type: "raster-dem", | ||||||
|                 url: |                 url: | ||||||
|                     "https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" + |                     "https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" + | ||||||
|                     Constants.maptilerApiKey, |                     Constants.maptilerApiKey | ||||||
|             }) |             }) | ||||||
|             try { |             try { | ||||||
|                 while (!map?.isStyleLoaded()) { |                 while (!map?.isStyleLoaded()) { | ||||||
|                     await Utils.waitFor(250) |                     await Utils.waitFor(250) | ||||||
|                 } |                 } | ||||||
|                 map.setTerrain({ |                 map.setTerrain({ | ||||||
|                     source: id, |                     source: id | ||||||
|                 }) |                 }) | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 console.error(e) |                 console.error(e) | ||||||
|  | @ -680,10 +704,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public flyTo(lon: number, lat: number, zoom: number){ |     public flyTo(lon: number, lat: number, zoom: number) { | ||||||
|         this._maplibreMap.data?.flyTo({ |         this.lockZoom() | ||||||
|             zoom, |         window.requestAnimationFrame(() => { | ||||||
|             center: [lon, lat], |             this._maplibreMap.data?.flyTo({ | ||||||
|  |                 zoom, | ||||||
|  |                 center: [lon, lat] | ||||||
|  |             }) | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Store, UIEventSource } from "../Logic/UIEventSource" | import { Store, UIEventSource } from "../Logic/UIEventSource" | ||||||
| import BaseUIElement from "./BaseUIElement" | import BaseUIElement from "./BaseUIElement" | ||||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | import LayoutConfig, { MinimalLayoutInformation } from "../Models/ThemeConfig/LayoutConfig" | ||||||
| import { | import { | ||||||
|     FeatureSource, |     FeatureSource, | ||||||
|     IndexedFeatureSource, |     IndexedFeatureSource, | ||||||
|  | @ -87,6 +87,7 @@ export interface SpecialVisualizationState { | ||||||
|         readonly showAllQuestionsAtOnce: UIEventSource<boolean> |         readonly showAllQuestionsAtOnce: UIEventSource<boolean> | ||||||
|         readonly preferencesAsTags: UIEventSource<Record<string, string>> |         readonly preferencesAsTags: UIEventSource<Record<string, string>> | ||||||
|         readonly language: UIEventSource<string> |         readonly language: UIEventSource<string> | ||||||
|  |         readonly recentlyVisitedThemes: Store<string[]> | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     readonly availableLayers: Store<RasterLayerPolygon[]> |     readonly availableLayers: Store<RasterLayerPolygon[]> | ||||||
|  |  | ||||||
|  | @ -62,7 +62,7 @@ | ||||||
|             return "offline" |             return "offline" | ||||||
|         } |         } | ||||||
|       }), |       }), | ||||||
|       message: osmApi, |       message: osmApi | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -90,7 +90,7 @@ | ||||||
|         } |         } | ||||||
|         const files: string[] = s["success"]["allFiles"] |         const files: string[] = s["success"]["allFiles"] | ||||||
|         return "Contains " + (files.length ?? "no") + " files" |         return "Contains " + (files.length ?? "no") + " files" | ||||||
|       }), |       }) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|   { |   { | ||||||
|  | @ -106,7 +106,7 @@ | ||||||
|           return "degraded" |           return "degraded" | ||||||
|         } |         } | ||||||
|       }), |       }), | ||||||
|       message: simpleMessage(testDownload(Constants.GeoIpServer + "/ip")), |       message: simpleMessage(testDownload(Constants.GeoIpServer + "/ip")) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -125,7 +125,7 @@ | ||||||
|         } |         } | ||||||
|         return "degraded" |         return "degraded" | ||||||
|       }), |       }), | ||||||
|       message: simpleMessage(status), |       message: simpleMessage(status) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -144,7 +144,7 @@ | ||||||
|         } |         } | ||||||
|         return "online" |         return "online" | ||||||
|       }), |       }), | ||||||
|       message: simpleMessage(status), |       message: simpleMessage(status) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -183,7 +183,7 @@ | ||||||
| 
 | 
 | ||||||
|         const json = JSON.stringify(s["success"], null, "  ") |         const json = JSON.stringify(s["success"], null, "  ") | ||||||
|         return "Database is " + Math.floor(timediffDays) + " days out of sync\n\n" + json |         return "Database is " + Math.floor(timediffDays) + " days out of sync\n\n" + json | ||||||
|       }), |       }) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -202,7 +202,45 @@ | ||||||
|         } |         } | ||||||
|         return "degraded" |         return "degraded" | ||||||
|       }), |       }), | ||||||
|       message: status.map((s) => JSON.stringify(s)), |       message: status.map((s) => JSON.stringify(s)) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   { | ||||||
|  |     const s = Constants.nominatimEndpoint | ||||||
|  |     const status = testDownload(s + "/search.php?q=Brugge") | ||||||
|  |     services.push({ | ||||||
|  |       name: s, | ||||||
|  |       message: simpleMessage(status), | ||||||
|  |       status: status.mapD(s => { | ||||||
|  |         if (s["error"]) { | ||||||
|  |           return "offline" | ||||||
|  |         } | ||||||
|  |         const data = s["success"] | ||||||
|  |         if (Array.isArray(data)) { | ||||||
|  |           return "online" | ||||||
|  |         } | ||||||
|  |         return "degraded" | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   { | ||||||
|  |     const s = Constants.photonEndpoint | ||||||
|  |     const status = testDownload(s + "/api/?q=Brugge") | ||||||
|  |     services.push({ | ||||||
|  |       name: s, | ||||||
|  |       status: status.mapD(s => { | ||||||
|  |         if (s["error"]) { | ||||||
|  |           return "offline" | ||||||
|  |         } | ||||||
|  |         const data = s["success"] | ||||||
|  |         if (Array.isArray(data.features) && data.features.length > 0) { | ||||||
|  |           return "online" | ||||||
|  |         } | ||||||
|  |         return "degraded" | ||||||
|  |       }), | ||||||
|  |       message: simpleMessage(status) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -228,7 +266,7 @@ | ||||||
| 
 | 
 | ||||||
|           return "online" |           return "online" | ||||||
|         }), |         }), | ||||||
|         message: simpleMessage(status), |         message: simpleMessage(status) | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | @ -241,7 +279,7 @@ | ||||||
|           return "online" |           return "online" | ||||||
|         } |         } | ||||||
|         return "offline" |         return "offline" | ||||||
|       }), |       }) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -263,14 +263,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         return res |         return res | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static NoNull<T>(array: T[] | undefined): T[] | undefined |     public static NoNull<T>(array: ReadonlyArray<T> | undefined): T[] | undefined | ||||||
|     public static NoNull<T>(array: undefined): undefined |     public static NoNull<T>(array: undefined): undefined | ||||||
|     public static NoNull<T>(array: T[]): T[] |     public static NoNull<T>(array: ReadonlyArray<T>): T[] | ||||||
|     public static NoNull<T>(array: T[]): NonNullable<T>[] { |     public static NoNull<T>(array: ReadonlyArray<T>): NonNullable<T>[] { | ||||||
|         return <any>array?.filter((o) => o !== undefined && o !== null) |         return <any>array?.filter((o) => o !== undefined && o !== null) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static Hist(array: string[]): Map<string, number> { |     public static Hist(array: ReadonlyArray<string>): Map<string, number> { | ||||||
|         const hist = new Map<string, number>() |         const hist = new Map<string, number>() | ||||||
|         for (const s of array) { |         for (const s of array) { | ||||||
|             hist.set(s, 1 + (hist.get(s) ?? 0)) |             hist.set(s, 1 + (hist.get(s) ?? 0)) | ||||||
|  |  | ||||||
							
								
								
									
										495
									
								
								src/index.css
									
										
									
									
									
								
							
							
						
						
									
										495
									
								
								src/index.css
									
										
									
									
									
								
							|  | @ -12,63 +12,63 @@ | ||||||
| @tailwind utilities; | @tailwind utilities; | ||||||
| 
 | 
 | ||||||
| :root { | :root { | ||||||
|   /*  |     /* | ||||||
|     * The main colour scheme of mapcomplete is configured here. |       * The main colour scheme of mapcomplete is configured here. | ||||||
|     * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. |       * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. | ||||||
|     */ |       */ | ||||||
| 
 | 
 | ||||||
|     /* No support for dark mode yet, we disable it to prevent some elements to suddenly toggle */ |     /* No support for dark mode yet, we disable it to prevent some elements to suddenly toggle */ | ||||||
|     color-scheme: only light; |     color-scheme: only light; | ||||||
| 
 | 
 | ||||||
|   /* Main color of the application: the background and text colours */ |     /* Main color of the application: the background and text colours */ | ||||||
|   --background-color: white; |     --background-color: white; | ||||||
|   /* Main text colour. Also styles some elements, such as the 'close popup'-button or 'back-arrow' (in mobile) */ |     /* Main text colour. Also styles some elements, such as the 'close popup'-button or 'back-arrow' (in mobile) */ | ||||||
|   --foreground-color: black; |     --foreground-color: black; | ||||||
| 
 | 
 | ||||||
|   /* A colour scheme to indicate an error or warning */ |     /* A colour scheme to indicate an error or warning */ | ||||||
|   --alert-color: #fee4d1; |     --alert-color: #fee4d1; | ||||||
|   --alert-foreground-color: var(--foreground-color); |     --alert-foreground-color: var(--foreground-color); | ||||||
| 
 | 
 | ||||||
|   --low-interaction-background: #eeeeee; |     --low-interaction-background: #eeeeee; | ||||||
|   --low-interaction-background-50: #eeeeee90; |     --low-interaction-background-50: #eeeeee90; | ||||||
|   --low-interaction-foreground: black; |     --low-interaction-foreground: black; | ||||||
|   --low-interaction-contrast: #ff00ff; |     --low-interaction-contrast: #ff00ff; | ||||||
| 
 | 
 | ||||||
|   --interactive-background: #dddddd; |     --interactive-background: #dddddd; | ||||||
|   --interactive-foreground: black; |     --interactive-foreground: black; | ||||||
|   --interactive-contrast: #ff00ff; |     --interactive-contrast: #ff00ff; | ||||||
| 
 | 
 | ||||||
|   --button-background: #282828; |     --button-background: #282828; | ||||||
|   --button-background-hover: #484848; |     --button-background-hover: #484848; | ||||||
|     --button-primary-background-hover: #353535; |     --button-primary-background-hover: #353535; | ||||||
| 
 | 
 | ||||||
|   --button-foreground: white; |     --button-foreground: white; | ||||||
|   --button-border-color: #F7F7F7; |     --button-border-color: #F7F7F7; | ||||||
|   --disabled: #B8B8B8; |     --disabled: #B8B8B8; | ||||||
|     --disabled-font: #B8B8B8; |     --disabled-font: #B8B8B8; | ||||||
| 
 | 
 | ||||||
|   /** |     /** | ||||||
|      * Base colour of interactive elements, mainly the 'subtle button' |        * Base colour of interactive elements, mainly the 'subtle button' | ||||||
|      * @deprecated |        * @deprecated | ||||||
|      */ |        */ | ||||||
|   --subtle-detail-color: #dbeafe; |     --subtle-detail-color: #dbeafe; | ||||||
|   --subtle-detail-color-contrast: black; |     --subtle-detail-color-contrast: black; | ||||||
|   --subtle-detail-color-light-contrast: lightgrey; |     --subtle-detail-color-light-contrast: lightgrey; | ||||||
| 
 | 
 | ||||||
|   --catch-detail-color: black; /*#3a3aeb;*/ |     --catch-detail-color: black; /*#3a3aeb;*/ | ||||||
|   --catch-detail-foregroundcolor: white; |     --catch-detail-foregroundcolor: white; | ||||||
|   --catch-detail-color-contrast: #fb3afb; |     --catch-detail-color-contrast: #fb3afb; | ||||||
| 
 | 
 | ||||||
|   --image-carousel-height: 350px; |     --image-carousel-height: 350px; | ||||||
| 
 | 
 | ||||||
|   /** Technical value, used by icon.svelte |     /** Technical value, used by icon.svelte | ||||||
|      */ |        */ | ||||||
|   --svg-color: #000000; |     --svg-color: #000000; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @font-face{ | @font-face { | ||||||
|     font-family:"Source Sans Pro"; |     font-family: "Source Sans Pro"; | ||||||
|     src:url("/assets/source-sans-pro.regular.ttf") format("woff"); |     src: url("/assets/source-sans-pro.regular.ttf") format("woff"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /***********************************************************************\ | /***********************************************************************\ | ||||||
|  | @ -76,13 +76,13 @@ | ||||||
| \***********************************************************************/ | \***********************************************************************/ | ||||||
| html, | html, | ||||||
| body { | body { | ||||||
|   height: 100%; |     height: 100%; | ||||||
|   min-height: 100vh; |     min-height: 100vh; | ||||||
|   min-height: -webkit-fill-available; |     min-height: -webkit-fill-available; | ||||||
|   margin: 0; |     margin: 0; | ||||||
|   padding: 0; |     padding: 0; | ||||||
|   background-color: var(--background-color); |     background-color: var(--background-color); | ||||||
|   color: var(--foreground-color); |     color: var(--foreground-color); | ||||||
| 
 | 
 | ||||||
|     font-family: 'Source Sans Pro'; |     font-family: 'Source Sans Pro'; | ||||||
|     font-style: normal; |     font-style: normal; | ||||||
|  | @ -93,52 +93,52 @@ body { | ||||||
| 
 | 
 | ||||||
| svg, | svg, | ||||||
| img { | img { | ||||||
|   box-sizing: content-box; |     box-sizing: content-box; | ||||||
|   width: 100%; |     width: 100%; | ||||||
|   height: 100%; |     height: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| li { | li { | ||||||
|   margin-left: 0.5em; |     margin-left: 0.5em; | ||||||
|   padding-left: 0.2em; |     padding-left: 0.2em; | ||||||
|   margin-top: 0.1em; |     margin-top: 0.1em; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| li::marker { | li::marker { | ||||||
|   content: "•"; |     content: "•"; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| h1 { | h1 { | ||||||
|   font-size: xx-large; |     font-size: xx-large; | ||||||
|   margin-top: 0.6em; |     margin-top: 0.6em; | ||||||
|   margin-bottom: 0.4em; |     margin-bottom: 0.4em; | ||||||
|   font-weight: bold; |     font-weight: bold; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| h2 { | h2 { | ||||||
|   font-size: x-large; |     font-size: x-large; | ||||||
|   margin-top: 0.5em; |     margin-top: 0.5em; | ||||||
|   margin-bottom: 0; /*Disable margin bottom to play nicely with accordeons from flowbite*/ |     margin-bottom: 0; /*Disable margin bottom to play nicely with accordeons from flowbite*/ | ||||||
|   font-weight: bold; |     font-weight: bold; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| h3 { | h3 { | ||||||
|   font-size: larger; |     font-size: larger; | ||||||
|   margin-top: 0.6em; |     margin-top: 0.6em; | ||||||
|   margin-bottom: 0; |     margin-bottom: 0; | ||||||
|   font-weight: bold; |     font-weight: bold; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| p { | p { | ||||||
|   padding-top: 0.1em; |     padding-top: 0.1em; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| input { | input { | ||||||
|   color: var(--foreground-color); |     color: var(--foreground-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| input[type="text"] { | input[type="text"] { | ||||||
|   width: 100%; |     width: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /************************* BIG CATEGORIES ********************************/ | /************************* BIG CATEGORIES ********************************/ | ||||||
|  | @ -149,33 +149,33 @@ input[type="text"] { | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| .subtle-background { | .subtle-background { | ||||||
|   background: var(--subtle-detail-color); |     background: var(--subtle-detail-color); | ||||||
|   color: var(--subtle-detail-color-contrast); |     color: var(--subtle-detail-color-contrast); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .normal-background { | .normal-background { | ||||||
|   background: var(--background-color); |     background: var(--background-color); | ||||||
|   color: var(--foreground-color); |     color: var(--foreground-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .low-interaction { | .low-interaction { | ||||||
|   background: var(--low-interaction-background); |     background: var(--low-interaction-background); | ||||||
|   color: var(--low-interaction-foreground); |     color: var(--low-interaction-foreground); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .interactive { | .interactive { | ||||||
|   background: var(--interactive-background); |     background: var(--interactive-background); | ||||||
|   color: var(--interactive-foreground); |     color: var(--interactive-foreground); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .border-interactive { | .border-interactive { | ||||||
|   border: 2px dashed var(--catch-detail-color-contrast); |     border: 2px dashed var(--catch-detail-color-contrast); | ||||||
|   border-radius: 0.5rem; |     border-radius: 0.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .border-region { | .border-region { | ||||||
|   border: 2px dashed var(--interactive-background); |     border: 2px dashed var(--interactive-background); | ||||||
|   border-radius: 0.5rem; |     border-radius: 0.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /******************* Styling of input elements **********************/ | /******************* Styling of input elements **********************/ | ||||||
|  | @ -244,9 +244,11 @@ button.disabled { | ||||||
|     color: var(--disabled-font); |     color: var(--disabled-font); | ||||||
|     cursor: unset; |     cursor: unset; | ||||||
| } | } | ||||||
|  | 
 | ||||||
| button.disabled svg path { | button.disabled svg path { | ||||||
|     transition: all 200ms; |     transition: all 200ms; | ||||||
| } | } | ||||||
|  | 
 | ||||||
| button.disabled svg path { | button.disabled svg path { | ||||||
|     fill: var(--disabled-font); |     fill: var(--disabled-font); | ||||||
|     stroke: var(--disabled-font); |     stroke: var(--disabled-font); | ||||||
|  | @ -294,49 +296,49 @@ button.unstyled { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .links-w-full a:not(.weblate-link), .links-w-full button.as-link { | .links-w-full a:not(.weblate-link), .links-w-full button.as-link { | ||||||
|   display: flex; |     display: flex; | ||||||
|   column-gap: 0.25rem; |     column-gap: 0.25rem; | ||||||
|   padding-left: 0.5rem; |     padding-left: 0.5rem; | ||||||
|   padding-right: 0.5rem; |     padding-right: 0.5rem; | ||||||
|   width: 100%; |     width: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| select { | select { | ||||||
|   border: 2px solid #00000000; |     border: 2px solid #00000000; | ||||||
|   color: var(--foreground-color) !important; |     color: var(--foreground-color) !important; | ||||||
|   background-color: var(--low-interaction-background) !important; |     background-color: var(--low-interaction-background) !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| select:hover { | select:hover { | ||||||
|   border-color: var(--catch-detail-color-contrast); |     border-color: var(--catch-detail-color-contrast); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .neutral-label { | .neutral-label { | ||||||
|   /** This label styles as normal text. It's power comes from the many :not(.neutral-label) entries. |     /** This label styles as normal text. It's power comes from the many :not(.neutral-label) entries. | ||||||
|      * Placed here for autocompletion |        * Placed here for autocompletion | ||||||
|      */ |        */ | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| label:not(.neutral-label):not(.button) { | label:not(.neutral-label):not(.button) { | ||||||
|   /** |     /** | ||||||
|      * Label should _contain_ the input element |        * Label should _contain_ the input element | ||||||
|      */ |        */ | ||||||
|   padding: 0.25rem; |     padding: 0.25rem; | ||||||
|   padding-right: 0.5rem; |     padding-right: 0.5rem; | ||||||
|   padding-left: 0.5rem; |     padding-left: 0.5rem; | ||||||
|   margin:0.25rem; |     margin: 0.25rem; | ||||||
|   border-radius: 0.5rem; |     border-radius: 0.5rem; | ||||||
|   width: 100%; |     width: 100%; | ||||||
|   box-sizing: border-box; |     box-sizing: border-box; | ||||||
|   transition: all 250ms; |     transition: all 250ms; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| label.button { | label.button { | ||||||
|   width: 100%; |     width: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| label:hover:not(.neutral-label) { | label:hover:not(.neutral-label) { | ||||||
|   background-color: var(--low-interaction-background); |     background-color: var(--low-interaction-background); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -346,7 +348,7 @@ label.checked:not(.neutral-label) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| textarea { | textarea { | ||||||
|   color: black; |     color: black; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| h2.group { | h2.group { | ||||||
|  | @ -368,97 +370,97 @@ h2.group { | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| .thanks { | .thanks { | ||||||
|   /* The class to indicate 'operation successful' or 'thank you for contributing' */ |     /* The class to indicate 'operation successful' or 'thank you for contributing' */ | ||||||
|   font-weight: bold; |     font-weight: bold; | ||||||
|   border-radius: 1em; |     border-radius: 1em; | ||||||
|   margin: 0.25em; |     margin: 0.25em; | ||||||
|   text-align: center; |     text-align: center; | ||||||
|   padding: 0.25rem; |     padding: 0.25rem; | ||||||
|   padding-left: 0.5rem; |     padding-left: 0.5rem; | ||||||
|   padding-right: 0.5rem; |     padding-right: 0.5rem; | ||||||
|   border: 3px dotted #58cd27; |     border: 3px dotted #58cd27; | ||||||
|   background-color: #58cd2722; |     background-color: #58cd2722; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .alert { | .alert { | ||||||
|   /* The class to convey important information, e.g. 'invalid', 'something went wrong', 'warning: testmode', ... */ |     /* The class to convey important information, e.g. 'invalid', 'something went wrong', 'warning: testmode', ... */ | ||||||
|   background-color: var(--alert-color); |     background-color: var(--alert-color); | ||||||
|   color: var(--alert-foreground-color); |     color: var(--alert-foreground-color); | ||||||
|   font-weight: bold; |     font-weight: bold; | ||||||
|   border-radius: 1em; |     border-radius: 1em; | ||||||
|   margin: 0.25em; |     margin: 0.25em; | ||||||
|   text-align: center; |     text-align: center; | ||||||
|   padding: 0.15em 0.3em; |     padding: 0.15em 0.3em; | ||||||
|   border: 2px dotted #ff9143; |     border: 2px dotted #ff9143; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .warning { | .warning { | ||||||
|   /* The class to convey important information, but not as grave as 'alert' */ |     /* The class to convey important information, but not as grave as 'alert' */ | ||||||
|   background-color: var(--low-interaction-background); |     background-color: var(--low-interaction-background); | ||||||
|   color: var(--alert-foreground-color); |     color: var(--alert-foreground-color); | ||||||
|   font-weight: bold; |     font-weight: bold; | ||||||
|   border-radius: 1em; |     border-radius: 1em; | ||||||
|   margin: 0.25em; |     margin: 0.25em; | ||||||
|   text-align: center; |     text-align: center; | ||||||
|   padding: 0.15em 0.3em; |     padding: 0.15em 0.3em; | ||||||
|   border: 3px dotted #ff9143; |     border: 3px dotted #ff9143; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .low-interaction .warning { | .low-interaction .warning { | ||||||
|   background-color: var(--interactive-background); |     background-color: var(--interactive-background); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .information { | .information { | ||||||
|   /* The class to convey important information which does _not_ denote an error... */ |     /* The class to convey important information which does _not_ denote an error... */ | ||||||
|   background-color: var(--low-interaction-background); |     background-color: var(--low-interaction-background); | ||||||
|   color: var(--alert-foreground-color); |     color: var(--alert-foreground-color); | ||||||
|   border-radius: 1em; |     border-radius: 1em; | ||||||
|   margin: 0.25em; |     margin: 0.25em; | ||||||
|   text-align: center; |     text-align: center; | ||||||
|   padding: 0.15em 0.3em; |     padding: 0.15em 0.3em; | ||||||
|   border: 3px dotted var(--catch-detail-color-contrast); |     border: 3px dotted var(--catch-detail-color-contrast); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .low-interaction .interactive { | .low-interaction .interactive { | ||||||
|   background-color: var(--interactive-background); |     background-color: var(--interactive-background); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .subtle { | .subtle { | ||||||
|   /* For all information that is not important for 99% of the users */ |     /* For all information that is not important for 99% of the users */ | ||||||
|   color: #666; |     color: #666; | ||||||
|   font-weight: normal; |     font-weight: normal; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .low-interaction .subtle { | .low-interaction .subtle { | ||||||
|   color: #444; |     color: #444; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .interactive .subtle { | .interactive .subtle { | ||||||
|   color: #333; |     color: #333; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .link-underline .subtle a { | .link-underline .subtle a { | ||||||
|   text-decoration: underline 1px #7193bb88; |     text-decoration: underline 1px #7193bb88; | ||||||
|   -webkit-text-decoration: underline; |     -webkit-text-decoration: underline; | ||||||
|   color: #7193bb; |     color: #7193bb; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .literal-code, | .literal-code, | ||||||
| code { | code { | ||||||
|   /* A codeblock */ |     /* A codeblock */ | ||||||
|   display: inline-block; |     display: inline-block; | ||||||
|   background-color: lightgray; |     background-color: lightgray; | ||||||
|   padding: 0.1rem; |     padding: 0.1rem; | ||||||
|   padding-left: 0.35rem; |     padding-left: 0.35rem; | ||||||
|   padding-right: 0.35rem; |     padding-right: 0.35rem; | ||||||
|   word-break: break-word; |     word-break: break-word; | ||||||
|   color: black; |     color: black; | ||||||
|   box-sizing: border-box; |     box-sizing: border-box; | ||||||
|   font-family: monospace; |     font-family: monospace; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .interactive .literal-code { | .interactive .literal-code { | ||||||
|   background-color: #b3b3b3; |     background-color: #b3b3b3; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /************************** UTILITY ************************/ | /************************** UTILITY ************************/ | ||||||
|  | @ -468,98 +470,104 @@ code { | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| .text-white a { | .text-white a { | ||||||
|   /* Used solely in 'imageAttribution'  and in many themes*/ |     /* Used solely in 'imageAttribution'  and in many themes*/ | ||||||
|   color: var(--background-color); |     color: var(--background-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .bg-black-transparent { | .bg-black-transparent { | ||||||
|   background-color: #00000088; |     background-color: #00000088; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .block-ruby { | .block-ruby { | ||||||
|   display: block ruby; |     display: block ruby; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .rounded-left-full { | .rounded-left-full { | ||||||
|   border-bottom-left-radius: 999rem; |     border-bottom-left-radius: 999rem; | ||||||
|   border-top-left-radius: 999rem; |     border-top-left-radius: 999rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .rounded-right-full { | .rounded-right-full { | ||||||
|   border-bottom-right-radius: 999rem; |     border-bottom-right-radius: 999rem; | ||||||
|   border-top-right-radius: 999rem; |     border-top-right-radius: 999rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .no-images img { | .no-images img { | ||||||
|   /* Used solely in 'imageAttribution' and in many themes for the label*/ |     /* Used solely in 'imageAttribution' and in many themes for the label*/ | ||||||
|   display: none; |     display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .weblate-link { | .weblate-link { | ||||||
|   display: inline-block; |     display: inline-block; | ||||||
|   flex-shrink: 0; |     flex-shrink: 0; | ||||||
|   margin: 0; |     margin: 0; | ||||||
|   padding: 0.25rem; |     padding: 0.25rem; | ||||||
|   width: 1.2rem; |     width: 1.2rem; | ||||||
|   height: 1.2rem; |     height: 1.2rem; | ||||||
|   border: unset; |     border: unset; | ||||||
|   border-radius: 5rem; |     border-radius: 5rem; | ||||||
|   backdrop-filter: var(--low-interaction-background); |     backdrop-filter: var(--low-interaction-background); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .no-weblate .weblate-link { | .no-weblate .weblate-link { | ||||||
|   display: none !important; |     display: none !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .link-underline a { | .link-underline a { | ||||||
|   text-decoration: underline 1px var(--foreground-color); |     text-decoration: underline 1px var(--foreground-color); | ||||||
|   -webkit-text-decoration: underline; |     -webkit-text-decoration: underline; | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| a.link-underline { | a.link-underline { | ||||||
|   text-decoration: underline 1px var(--foreground-color); |     text-decoration: underline 1px var(--foreground-color); | ||||||
|   -webkit-text-decoration: underline; |     -webkit-text-decoration: underline; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .link-no-underline a, a.link-no-underline { | .link-no-underline a, a.link-no-underline { | ||||||
|   text-decoration: none; |     text-decoration: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .disable-links a { | .disable-links a { | ||||||
|   pointer-events: none; |     pointer-events: none; | ||||||
|   text-decoration: none !important; |     text-decoration: none !important; | ||||||
|   color: var(--subtle-detail-color-contrast) !important; |     color: var(--subtle-detail-color-contrast) !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .enable-links a { | .enable-links a { | ||||||
|   pointer-events: unset; |     pointer-events: unset; | ||||||
|   text-decoration: underline !important; |     text-decoration: underline !important; | ||||||
|   -webkit-text-decoration: underline !important; |     -webkit-text-decoration: underline !important; | ||||||
|   color: unset !important; |     color: unset !important; | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | a:hover { | ||||||
|  |     background-color: var(--low-interaction-background); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| .disable-links a.must-link, | .disable-links a.must-link, | ||||||
| .disable-links .must-link a { | .disable-links .must-link a { | ||||||
|   /* Hide links if they are disabled */ |     /* Hide links if they are disabled */ | ||||||
|   display: none; |     display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .zebra-table tr:nth-child(even) { | .zebra-table tr:nth-child(even) { | ||||||
|   background-color: #f2f2f2; |     background-color: #f2f2f2; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /************************* MISC ELEMENTS *************************/ | /************************* MISC ELEMENTS *************************/ | ||||||
| 
 | 
 | ||||||
| .selected svg:not(.noselect *) path.selectable { | .selected svg:not(.noselect *) path.selectable { | ||||||
|   /* A marker on the map gets the 'selected' class when it's properties are displayed |     /* A marker on the map gets the 'selected' class when it's properties are displayed | ||||||
|     */ |       */ | ||||||
|   stroke: white !important; |     stroke: white !important; | ||||||
|   stroke-width: 20px !important; |     stroke-width: 20px !important; | ||||||
|   overflow: visible !important; |     overflow: visible !important; | ||||||
|   -webkit-animation: glowing-drop-shadow 1s ease-in-out infinite alternate; |     -webkit-animation: glowing-drop-shadow 1s ease-in-out infinite alternate; | ||||||
|   -moz-animation: glowing-drop-shadow 1s ease-in-out infinite alternate; |     -moz-animation: glowing-drop-shadow 1s ease-in-out infinite alternate; | ||||||
|   animation: glowing-drop-shadow 1s ease-in-out infinite alternate; |     animation: glowing-drop-shadow 1s ease-in-out infinite alternate; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .selected .light-icon svg:not(.noselect *) path.selectable { | .selected .light-icon svg:not(.noselect *) path.selectable { | ||||||
|  | @ -568,76 +576,75 @@ a.link-underline { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .selected svg { | .selected svg { | ||||||
|   /* A marker on the map gets the 'selected' class when it's properties are displayed |     /* A marker on the map gets the 'selected' class when it's properties are displayed | ||||||
|     */ |       */ | ||||||
|   overflow: visible !important; |     overflow: visible !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| svg.apply-fill path { | svg.apply-fill path { | ||||||
|   fill: var(--svg-color); |     fill: var(--svg-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .compass_arrow { | .compass_arrow { | ||||||
|   width: calc(2.5rem - 1px); |     width: calc(2.5rem - 1px); | ||||||
|   height: calc(2.5rem - 1px); |     height: calc(2.5rem - 1px); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @media (min-width: 640px) { | @media (min-width: 640px) { | ||||||
|   .compass_arrow { |     .compass_arrow { | ||||||
|     width: calc(2.75rem - 1px); |         width: calc(2.75rem - 1px); | ||||||
|     height: calc(2.75rem - 1px); |         height: calc(2.75rem - 1px); | ||||||
|   } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @-webkit-keyframes glowing-drop-shadow { | @-webkit-keyframes glowing-drop-shadow { | ||||||
|   from { |     from { | ||||||
|     filter: drop-shadow(5px 5px 60px rgb(128 128 128 / 0.6)); |         filter: drop-shadow(5px 5px 60px rgb(128 128 128 / 0.6)); | ||||||
|   } |     } | ||||||
|   to { |     to { | ||||||
|     filter: drop-shadow(5px 5px 80px rgb(0.5 0.5 0.5 / 0.8)); |         filter: drop-shadow(5px 5px 80px rgb(0.5 0.5 0.5 / 0.8)); | ||||||
|   } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @keyframes slide { | @keyframes slide { | ||||||
|   /* This is the animation on the marker to add a new point - it slides through all the possible presets */ |     /* This is the animation on the marker to add a new point - it slides through all the possible presets */ | ||||||
|   from { |     from { | ||||||
|     transform: translateX(0%); |         transform: translateX(0%); | ||||||
|   } |     } | ||||||
| 
 | 
 | ||||||
|   to { |     to { | ||||||
|     transform: translateX(calc(-100% + 42px)); |         transform: translateX(calc(-100% + 42px)); | ||||||
|   } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| /************************* LEGACY MARKER - CLEANUP BELOW ********************************/ | /************************* LEGACY MARKER - CLEANUP BELOW ********************************/ | ||||||
| 
 | 
 | ||||||
| .slideshow-item img { | .slideshow-item img { | ||||||
|   /* Legacy: should be replace when the image element is ported to Svelte*/ |     /* Legacy: should be replace when the image element is ported to Svelte*/ | ||||||
|   height: var(--image-carousel-height); |     height: var(--image-carousel-height); | ||||||
|   width: unset; |     width: unset; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .animate-height { | .animate-height { | ||||||
|   /* Legacy: should be replaced by headlessui disclosure in time */ |     /* Legacy: should be replaced by headlessui disclosure in time */ | ||||||
|   transition: max-height 0.5s ease-in-out; |     transition: max-height 0.5s ease-in-out; | ||||||
|   overflow-y: hidden; |     overflow-y: hidden; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .min-h-32 { | .min-h-32 { | ||||||
|   min-height: 8rem; |     min-height: 8rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .max-w-full { | .max-w-full { | ||||||
|   max-width: 100%; |     max-width: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /************************* Experimental support for foldable devices ********************************/ | /************************* Experimental support for foldable devices ********************************/ | ||||||
| @media (horizontal-viewport-segments: 2) { | @media (horizontal-viewport-segments: 2) { | ||||||
|   .theme-list { |     .theme-list { | ||||||
|     display: grid; |         display: grid; | ||||||
|     grid-auto-flow: row; |         grid-auto-flow: row; | ||||||
|     grid-template-columns: repeat(2, minmax(0, 1fr)); |         grid-template-columns: repeat(2, minmax(0, 1fr)); | ||||||
|   } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue