forked from MapComplete/MapComplete
		
	Huge refactoring of state and initial UI setup
This commit is contained in:
		
							parent
							
								
									4e43673de5
								
							
						
					
					
						commit
						eff6b5bfad
					
				
					 37 changed files with 5232 additions and 4907 deletions
				
			
		
							
								
								
									
										441
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										441
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -1,448 +1,19 @@ | |||
| import {Utils} from "./Utils"; | ||||
| import {ElementStorage} from "./Logic/ElementStorage"; | ||||
| import {Changes} from "./Logic/Osm/Changes"; | ||||
| import {OsmConnection} from "./Logic/Osm/OsmConnection"; | ||||
| import Locale from "./UI/i18n/Locale"; | ||||
| import {UIEventSource} from "./Logic/UIEventSource"; | ||||
| import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; | ||||
| import {QueryParameters} from "./Logic/Web/QueryParameters"; | ||||
| import {MangroveIdentity} from "./Logic/Web/MangroveReviews"; | ||||
| import InstalledThemes from "./Logic/Actors/InstalledThemes"; | ||||
| import BaseLayer from "./Models/BaseLayer"; | ||||
| import Loc from "./Models/Loc"; | ||||
| import Constants from "./Models/Constants"; | ||||
| import TitleHandler from "./Logic/Actors/TitleHandler"; | ||||
| import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; | ||||
| import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; | ||||
| import FilteredLayer from "./Models/FilteredLayer"; | ||||
| import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor"; | ||||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; | ||||
| import {BBox} from "./Logic/BBox"; | ||||
| import SelectedElementTagsUpdater from "./Logic/Actors/SelectedElementTagsUpdater"; | ||||
| import TilesourceConfig from "./Models/ThemeConfig/TilesourceConfig"; | ||||
| import FeaturePipelineState from "./Logic/State/FeaturePipelineState"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains the global state: a bunch of UI-event sources | ||||
|  */ | ||||
| 
 | ||||
| export default class State { | ||||
|     // The singleton of the global state
 | ||||
|     public static state: State; | ||||
| 
 | ||||
|     public readonly layoutToUse : LayoutConfig; | ||||
| 
 | ||||
|     /** | ||||
|      The mapping from id -> UIEventSource<properties> | ||||
| export default class State extends FeaturePipelineState { | ||||
|     /* The singleton of the global state | ||||
|      */ | ||||
|     public allElements: ElementStorage = new ElementStorage(); | ||||
|     /** | ||||
|      THe change handler | ||||
|      */ | ||||
|     public changes: Changes = new Changes(); | ||||
|     /** | ||||
|      The leaflet instance of the big basemap | ||||
|      */ | ||||
|     public leafletMap = new UIEventSource<L.Map>(undefined, "leafletmap"); | ||||
|     /** | ||||
|      * Background layer id | ||||
|      */ | ||||
|     public availableBackgroundLayers: UIEventSource<BaseLayer[]>; | ||||
|     /** | ||||
|      The user credentials | ||||
|      */ | ||||
|     public osmConnection: OsmConnection; | ||||
| 
 | ||||
|     public mangroveIdentity: MangroveIdentity; | ||||
| 
 | ||||
|     public favouriteLayers: UIEventSource<string[]>; | ||||
| 
 | ||||
|     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers"); | ||||
| 
 | ||||
|     public  overlayToggles : { config: TilesourceConfig, isDisplayed: UIEventSource<boolean>}[] | ||||
|      | ||||
|     /** | ||||
|      The latest element that was selected | ||||
|      */ | ||||
|     public readonly selectedElement = new UIEventSource<any>( | ||||
|         undefined, | ||||
|         "Selected element" | ||||
|     ); | ||||
| 
 | ||||
|     public readonly featureSwitchUserbadge: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchSearch: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchBackgroundSlection: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchAddNew: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchIframe: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchMoreQuests: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchShareScreen: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchGeolocation: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchIsTesting: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchIsDebugging: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchApiURL: UIEventSource<string>; | ||||
|     public readonly featureSwitchFilter: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchEnableExport: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchFakeUser: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchExportAsPdf: UIEventSource<boolean>; | ||||
|     public readonly overpassUrl: UIEventSource<string[]>; | ||||
|     public readonly overpassTimeout: UIEventSource<number>; | ||||
|      | ||||
|      | ||||
|     public readonly overpassMaxZoom: UIEventSource<number> = new UIEventSource<number>(17, "overpass-max-zoom: point to switch between OSM-api and overpass"); | ||||
| 
 | ||||
|     public featurePipeline: FeaturePipeline; | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * The map location: currently centered lat, lon and zoom | ||||
|      */ | ||||
|     public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl"); | ||||
| 
 | ||||
|     /** | ||||
|      * The current visible extent of the screen | ||||
|      */ | ||||
|     public readonly currentBounds = new UIEventSource<BBox>(undefined) | ||||
| 
 | ||||
|     public backgroundLayer; | ||||
|     public readonly backgroundLayerId: UIEventSource<string>; | ||||
| 
 | ||||
|     /* Last location where a click was registered | ||||
|      */ | ||||
|     public readonly LastClickLocation: UIEventSource<{ | ||||
|         lat: number; | ||||
|         lon: number; | ||||
|     }> = new UIEventSource<{ lat: number; lon: number }>(undefined); | ||||
| 
 | ||||
|     /** | ||||
|      * The location as delivered by the GPS | ||||
|      */ | ||||
|     public currentGPSLocation: UIEventSource<{ | ||||
|         latlng: { lat: number; lng: number }; | ||||
|         accuracy: number; | ||||
|     }> = new UIEventSource<{ | ||||
|         latlng: { lat: number; lng: number }; | ||||
|         accuracy: number; | ||||
|     }>(undefined); | ||||
|     public layoutDefinition: string; | ||||
|     public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; | ||||
| 
 | ||||
|     public downloadControlIsOpened: UIEventSource<boolean> = | ||||
|         QueryParameters.GetQueryParameter( | ||||
|             "download-control-toggle", | ||||
|             "false", | ||||
|             "Whether or not the download panel is shown" | ||||
|         ).map<boolean>( | ||||
|             (str) => str !== "false", | ||||
|             [], | ||||
|             (b) => "" + b | ||||
|         ); | ||||
| 
 | ||||
|     public filterIsOpened: UIEventSource<boolean> = | ||||
|         QueryParameters.GetQueryParameter( | ||||
|             "filter-toggle", | ||||
|             "false", | ||||
|             "Whether or not the filter view is shown" | ||||
|         ).map<boolean>( | ||||
|             (str) => str !== "false", | ||||
|             [], | ||||
|             (b) => "" + b | ||||
|         ); | ||||
| 
 | ||||
|     public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter( | ||||
|         "tab", | ||||
|         "0", | ||||
|         `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)` | ||||
|     ).map<number>( | ||||
|         (str) => (isNaN(Number(str)) ? 0 : Number(str)), | ||||
|         [], | ||||
|         (n) => "" + n | ||||
|     ); | ||||
|     public static state: FeaturePipelineState; | ||||
| 
 | ||||
|     constructor(layoutToUse: LayoutConfig) { | ||||
|         const self = this; | ||||
|         this.layoutToUse  = layoutToUse; | ||||
|         super(layoutToUse) | ||||
|     } | ||||
| 
 | ||||
|         // -- Location control initialization
 | ||||
|         { | ||||
|             const zoom = State.asFloat( | ||||
|                 QueryParameters.GetQueryParameter( | ||||
|                     "z", | ||||
|                     "" + (layoutToUse?.startZoom ?? 1), | ||||
|                     "The initial/current zoom level" | ||||
|                 ).syncWith(LocalStorageSource.Get("zoom")) | ||||
|             ); | ||||
|             const lat = State.asFloat( | ||||
|                 QueryParameters.GetQueryParameter( | ||||
|                     "lat", | ||||
|                     "" + (layoutToUse?.startLat ?? 0), | ||||
|                     "The initial/current latitude" | ||||
|                 ).syncWith(LocalStorageSource.Get("lat")) | ||||
|             ); | ||||
|             const lon = State.asFloat( | ||||
|                 QueryParameters.GetQueryParameter( | ||||
|                     "lon", | ||||
|                     "" + (layoutToUse?.startLon ?? 0), | ||||
|                     "The initial/current longitude of the app" | ||||
|                 ).syncWith(LocalStorageSource.Get("lon")) | ||||
|             ); | ||||
| 
 | ||||
|             this.locationControl.setData({ | ||||
|                 zoom: Utils.asFloat(zoom.data), | ||||
|                 lat: Utils.asFloat(lat.data), | ||||
|                 lon: Utils.asFloat(lon.data), | ||||
|             }) | ||||
|             this.locationControl.addCallback((latlonz) => { | ||||
|                 // Sync th location controls
 | ||||
|                 zoom.setData(latlonz.zoom); | ||||
|                 lat.setData(latlonz.lat); | ||||
|                 lon.setData(latlonz.lon); | ||||
|             }); | ||||
|              | ||||
|         } | ||||
| 
 | ||||
|         // Helper function to initialize feature switches
 | ||||
|         function featSw( | ||||
|             key: string, | ||||
|             deflt: (layout: LayoutConfig) => boolean, | ||||
|             documentation: string | ||||
|         ): UIEventSource<boolean> { | ||||
|              | ||||
|             const defaultValue = deflt(self.layoutToUse); | ||||
|             const queryParam = QueryParameters.GetQueryParameter( | ||||
|                 key, | ||||
|                 "" + defaultValue, | ||||
|                 documentation | ||||
|             ); | ||||
| 
 | ||||
|             // It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
 | ||||
|             return queryParam.map((str) => | ||||
|                 str === undefined ? defaultValue : str !== "false" | ||||
|             ) | ||||
|     | ||||
|         } | ||||
| 
 | ||||
|         // Feature switch initialization - not as a function as the UIEventSources are readonly
 | ||||
|         { | ||||
|             this.featureSwitchUserbadge = featSw( | ||||
|                 "fs-userbadge", | ||||
|                 (layoutToUse) => layoutToUse?.enableUserBadge ?? true, | ||||
|                 "Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode." | ||||
|             ); | ||||
|             this.featureSwitchSearch = featSw( | ||||
|                 "fs-search", | ||||
|                 (layoutToUse) => layoutToUse?.enableSearch ?? true, | ||||
|                 "Disables/Enables the search bar" | ||||
|             ); | ||||
|             this.featureSwitchBackgroundSlection = featSw( | ||||
|                 "fs-background", | ||||
|                 (layoutToUse) => layoutToUse?.enableBackgroundLayerSelection ?? true, | ||||
|                 "Disables/Enables the background layer control" | ||||
|             ); | ||||
| 
 | ||||
|             this.featureSwitchFilter = featSw( | ||||
|                 "fs-filter", | ||||
|                 (layoutToUse) => layoutToUse?.enableLayers ?? true, | ||||
|                 "Disables/Enables the filter" | ||||
|             ); | ||||
|             this.featureSwitchAddNew = featSw( | ||||
|                 "fs-add-new", | ||||
|                 (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true, | ||||
|                 "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)" | ||||
|             ); | ||||
|             this.featureSwitchWelcomeMessage = featSw( | ||||
|                 "fs-welcome-message", | ||||
|                 () => true, | ||||
|                 "Disables/enables the help menu or welcome message" | ||||
|             ); | ||||
|             this.featureSwitchIframe = featSw( | ||||
|                 "fs-iframe", | ||||
|                 () => false, | ||||
|                 "Disables/Enables the iframe-popup" | ||||
|             ); | ||||
|             this.featureSwitchMoreQuests = featSw( | ||||
|                 "fs-more-quests", | ||||
|                 (layoutToUse) => layoutToUse?.enableMoreQuests ?? true, | ||||
|                 "Disables/Enables the 'More Quests'-tab in the welcome message" | ||||
|             ); | ||||
|             this.featureSwitchShareScreen = featSw( | ||||
|                 "fs-share-screen", | ||||
|                 (layoutToUse) => layoutToUse?.enableShareScreen ?? true, | ||||
|                 "Disables/Enables the 'Share-screen'-tab in the welcome message" | ||||
|             ); | ||||
|             this.featureSwitchGeolocation = featSw( | ||||
|                 "fs-geolocation", | ||||
|                 (layoutToUse) => layoutToUse?.enableGeolocation ?? true, | ||||
|                 "Disables/Enables the geolocation button" | ||||
|             ); | ||||
|             this.featureSwitchShowAllQuestions = featSw( | ||||
|                 "fs-all-questions", | ||||
|                 (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, | ||||
|                 "Always show all questions" | ||||
|             ); | ||||
| 
 | ||||
|             this.featureSwitchEnableExport = featSw( | ||||
|                 "fs-export", | ||||
|                 (layoutToUse) => layoutToUse?.enableExportButton ?? false, | ||||
|                 "Enable the export as GeoJSON and CSV button" | ||||
|             ); | ||||
|             this.featureSwitchExportAsPdf = featSw( | ||||
|                 "fs-pdf", | ||||
|                 (layoutToUse) => layoutToUse?.enablePdfDownload ?? false, | ||||
|                 "Enable the PDF download button" | ||||
|             ); | ||||
| 
 | ||||
| 
 | ||||
|             this.featureSwitchIsTesting = QueryParameters.GetQueryParameter( | ||||
|                 "test", | ||||
|                 "false", | ||||
|                 "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org" | ||||
|             ).map( | ||||
|                 (str) => str === "true", | ||||
|                 [], | ||||
|                 (b) => "" + b | ||||
|             ); | ||||
| 
 | ||||
|             this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter( | ||||
|                 "debug", | ||||
|                 "false", | ||||
|                 "If true, shows some extra debugging help such as all the available tags on every object" | ||||
|             ).map( | ||||
|                 (str) => str === "true", | ||||
|                 [], | ||||
|                 (b) => "" + b | ||||
|             ); | ||||
| 
 | ||||
|             this.featureSwitchFakeUser = QueryParameters.GetQueryParameter("fake-user", "false", | ||||
|                 "If true, 'dryrun' mode is activated and a fake user account is loaded") | ||||
|                 .map(str => str === "true", [], b => "" + b); | ||||
| 
 | ||||
| 
 | ||||
|             this.featureSwitchApiURL = QueryParameters.GetQueryParameter( | ||||
|                 "backend", | ||||
|                 "osm", | ||||
|                 "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'" | ||||
|             ); | ||||
| 
 | ||||
|             this.overpassUrl = QueryParameters.GetQueryParameter("overpassUrl", | ||||
|                 (layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(",") , | ||||
|                 "Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter" | ||||
|             ).map(param => param.split(","), [], urls => urls.join(",")) | ||||
| 
 | ||||
|             this.overpassTimeout = QueryParameters.GetQueryParameter("overpassTimeout", | ||||
|                 "" + layoutToUse?.overpassTimeout, | ||||
|                 "Set a different timeout (in seconds) for queries in overpass") | ||||
|                 .map(str => Number(str), [], n => "" + n) | ||||
| 
 | ||||
|             this.featureSwitchUserbadge.addCallbackAndRun(userbadge => { | ||||
|                 if (!userbadge) { | ||||
|                     this.featureSwitchAddNew.setData(false) | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|         { | ||||
|             // Some other feature switches
 | ||||
|             const customCssQP = QueryParameters.GetQueryParameter( | ||||
|                 "custom-css", | ||||
|                 "", | ||||
|                 "If specified, the custom css from the given link will be loaded additionaly" | ||||
|             ); | ||||
|             if (customCssQP.data !== undefined && customCssQP.data !== "") { | ||||
|                 Utils.LoadCustomCss(customCssQP.data); | ||||
|             } | ||||
| 
 | ||||
|             this.backgroundLayerId = QueryParameters.GetQueryParameter( | ||||
|                 "background", | ||||
|                 layoutToUse?.defaultBackgroundId ?? "osm", | ||||
|                 "The id of the background layer to start with" | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.osmConnection = new OsmConnection({ | ||||
|             changes: this.changes, | ||||
|             dryRun: this.featureSwitchIsTesting.data, | ||||
|             fakeUser: this.featureSwitchFakeUser.data, | ||||
|             allElements: this.allElements, | ||||
|             oauth_token: QueryParameters.GetQueryParameter( | ||||
|                 "oauth_token", | ||||
|                 undefined, | ||||
|                 "Used to complete the login" | ||||
|             ), | ||||
|             layoutName: layoutToUse?.id, | ||||
|             osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         new ChangeToElementsActor(this.changes, this.allElements) | ||||
| 
 | ||||
|         new PendingChangesUploader(this.changes, this.selectedElement); | ||||
|          | ||||
|         new SelectedElementTagsUpdater(this) | ||||
| 
 | ||||
|         this.mangroveIdentity = new MangroveIdentity( | ||||
|             this.osmConnection.GetLongPreference("identity", "mangrove") | ||||
|         ); | ||||
| 
 | ||||
|         this.installedThemes = new InstalledThemes( | ||||
|             this.osmConnection | ||||
|         ).installedThemes; | ||||
| 
 | ||||
|         // Important: the favourite layers are initialized _after_ the installed themes, as these might contain an installedTheme
 | ||||
|         this.favouriteLayers = LocalStorageSource.Get("favouriteLayers") | ||||
|             .syncWith(this.osmConnection.GetLongPreference("favouriteLayers")) | ||||
|             .map( | ||||
|                 (str) => Utils.Dedup(str?.split(";")) ?? [], | ||||
|                 [], | ||||
|                 (layers) => Utils.Dedup(layers)?.join(";") | ||||
|             ); | ||||
| 
 | ||||
|         Locale.language.syncWith(this.osmConnection.GetPreference("language")); | ||||
| 
 | ||||
|         Locale.language | ||||
|             .addCallback((currentLanguage) => { | ||||
|                 const layoutToUse = self.layoutToUse; | ||||
|                 if (layoutToUse === undefined) { | ||||
|                     return; | ||||
|                 } | ||||
|                 if (this.layoutToUse.language.indexOf(currentLanguage) < 0) { | ||||
|                     console.log( | ||||
|                         "Resetting language to", | ||||
|                         layoutToUse.language[0], | ||||
|                         "as", | ||||
|                         currentLanguage, | ||||
|                         " is unsupported" | ||||
|                     ); | ||||
|                     // The current language is not supported -> switch to a supported one
 | ||||
|                     Locale.language.setData(layoutToUse.language[0]); | ||||
|                 } | ||||
|             }) | ||||
|             .ping(); | ||||
| 
 | ||||
|         new TitleHandler(this); | ||||
|          | ||||
|         this.overlayToggles = this.layoutToUse.tileLayerSources.filter(c => c.name !== undefined).map(c => ({ | ||||
|             config: c, | ||||
|             isDisplayed: QueryParameters.GetQueryParameter("overlay-"+c.id, ""+c.defaultState,"Wether or not the overlay "+c.id+" is shown").map(str => str === "true", [], b => ""+b) | ||||
|         })) | ||||
|     } | ||||
| 
 | ||||
|     private static asFloat(source: UIEventSource<string>): UIEventSource<number> { | ||||
|         return source.map( | ||||
|             (str) => { | ||||
|                 let parsed = parseFloat(str); | ||||
|                 return isNaN(parsed) ? undefined : parsed; | ||||
|             }, | ||||
|             [], | ||||
|             (fl) => { | ||||
|                 if (fl === undefined || isNaN(fl)) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|                 return ("" + fl).substr(0, 8); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue