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/GeoOperations"; /** * 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, "layoutToUse"); /** 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, "leafletmap"); /** * Background layer id */ public availableBackgroundLayers: UIEventSource; /** The user credentials */ public osmConnection: OsmConnection; public mangroveIdentity: MangroveIdentity; public favouriteLayers: UIEventSource; public filteredLayers: UIEventSource = new UIEventSource([], "filteredLayers"); /** The latest element that was selected */ public readonly selectedElement = new UIEventSource( undefined, "Selected element" ); public readonly featureSwitchUserbadge: UIEventSource; public readonly featureSwitchSearch: UIEventSource; public readonly featureSwitchBackgroundSlection: 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; public readonly featureSwitchIsDebugging: UIEventSource; public readonly featureSwitchShowAllQuestions: UIEventSource; public readonly featureSwitchApiURL: UIEventSource; public readonly featureSwitchFilter: UIEventSource; public readonly featureSwitchEnableExport: UIEventSource; public readonly featureSwitchFakeUser: UIEventSource; public readonly featureSwitchExportAsPdf: UIEventSource; public readonly overpassUrl: UIEventSource; public readonly overpassTimeout: UIEventSource; public readonly overpassMaxZoom: UIEventSource = new UIEventSource(undefined); public featurePipeline: FeaturePipeline; /** * The map location: currently centered lat, lon and zoom */ public readonly locationControl = new UIEventSource(undefined, "locationControl"); /** * The current visible extent of the screen */ public readonly currentBounds = new UIEventSource(undefined) public backgroundLayer; public readonly backgroundLayerId: UIEventSource; /* 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 = QueryParameters.GetQueryParameter( "download-control-toggle", "false", "Whether or not the download panel is shown" ).map( (str) => str !== "false", [], (b) => "" + b ); public filterIsOpened: UIEventSource = QueryParameters.GetQueryParameter( "filter-toggle", "false", "Whether or not the filter view 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.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); }); 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.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, "Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter" ) 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( this.featureSwitchIsTesting.data, this.featureSwitchFakeUser.data, QueryParameters.GetQueryParameter( "oauth_token", undefined, "Used to complete the login" ), layoutToUse?.id, true, // @ts-ignore this.featureSwitchApiURL.data ); this.allElements = new ElementStorage(); this.changes = new Changes(); new ChangeToElementsActor(this.changes, this.allElements) 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); } 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); } ); } }