import {UIElement} from "./UI/UIElement"; 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 LayoutConfig from "./Customizations/JSON/LayoutConfig"; import Hash from "./Logic/Web/Hash"; 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 UpdateFromOverpass from "./Logic/Actors/UpdateFromOverpass"; import LayerConfig from "./Customizations/JSON/LayerConfig"; import TitleHandler from "./Logic/Actors/TitleHandler"; import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; /** * 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 = new UIEventSource(undefined); /** The mapping from id -> UIEventSource */ public allElements: ElementStorage; /** THe change handler */ public changes: Changes; /** The leaflet instance of the big basemap */ public leafletMap = new UIEventSource(undefined); /** * Background layer id */ public availableBackgroundLayers: UIEventSource; /** The user credentials */ public osmConnection: OsmConnection; public mangroveIdentity: MangroveIdentity; public favouriteLayers: UIEventSource; public layerUpdater: UpdateFromOverpass; public filteredLayers: UIEventSource<{ readonly isDisplayed: UIEventSource, readonly layerDef: LayerConfig; }[]> = new UIEventSource<{ readonly isDisplayed: UIEventSource, readonly layerDef: LayerConfig; }[]>([]) /** * The message that should be shown at the center of the screen */ public readonly centerMessage = new UIEventSource(""); /** The latest element that was selected */ public readonly selectedElement = new UIEventSource(undefined) public readonly featureSwitchUserbadge: UIEventSource; public readonly featureSwitchSearch: UIEventSource; public readonly featureSwitchLayers: UIEventSource; public readonly featureSwitchAddNew: UIEventSource; public readonly featureSwitchWelcomeMessage: UIEventSource; public readonly featureSwitchIframe: UIEventSource; public readonly featureSwitchMoreQuests: UIEventSource; public readonly featureSwitchShareScreen: UIEventSource; public readonly featureSwitchGeolocation: UIEventSource; public readonly featureSwitchIsTesting: UIEventSource; /** * The map location: currently centered lat, lon and zoom */ public readonly locationControl = new UIEventSource(undefined); public backgroundLayer; /* 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 layerControlIsOpened: UIEventSource = QueryParameters.GetQueryParameter("layer-control-toggle", "false", "Whether or not the layer control is shown") .map((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( str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n ); constructor(layoutToUse: LayoutConfig) { const self = this; this.layoutToUse.setData(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 = new UIEventSource({ zoom: Utils.asFloat(zoom.data), lat: Utils.asFloat(lat.data), lon: Utils.asFloat(lon.data), }).addCallback((latlonz) => { zoom.setData(latlonz.zoom); lat.setData(latlonz.lat); lon.setData(latlonz.lon); }); this.layoutToUse.addCallback(layoutToUse => { const lcd = self.locationControl.data; lcd.zoom = lcd.zoom ?? layoutToUse?.startZoom; lcd.lat = lcd.lat ?? layoutToUse?.startLat; lcd.lon = lcd.lon ?? layoutToUse?.startLon; self.locationControl.ping(); }); } // Helper function to initialize feature switches function featSw(key: string, deflt: (layout: LayoutConfig) => boolean, documentation: string): UIEventSource { const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined, documentation); // I'm so sorry about someone trying to decipher this // It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened return UIEventSource.flatten( self.layoutToUse.map((layout) => { const defaultValue = deflt(layout); const queryParam = QueryParameters.GetQueryParameter(key, "" + defaultValue, documentation) return queryParam.map((str) => str === undefined ? defaultValue : (str !== "false")); }), [queryParameterSource]); } // 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.featureSwitchLayers = featSw("fs-layers", (layoutToUse) => layoutToUse?.enableLayers ?? true, "Disables/Enables the layer control"); 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.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.osmConnection = new OsmConnection( this.featureSwitchIsTesting.data, QueryParameters.GetQueryParameter("oauth_token", undefined, "Used to complete the login"), layoutToUse?.id, true ); this.allElements = new ElementStorage(); this.changes = new Changes(); new PendingChangesUploader(this.changes, this.selectedElement); 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.data; if (layoutToUse === undefined) { return; } if (this.layoutToUse.data.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.layoutToUse, this.selectedElement, this.allElements); } private static asFloat(source: UIEventSource): UIEventSource { 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); }) } }