forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			440 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			440 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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";
 | |
| 
 | |
| /**
 | |
|  * 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>
 | |
|      */
 | |
|     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");
 | |
| 
 | |
|     /**
 | |
|      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
 | |
|     );
 | |
| 
 | |
|     constructor(layoutToUse: LayoutConfig) {
 | |
|         const self = this;
 | |
|         this.layoutToUse  = 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);
 | |
|     }
 | |
| 
 | |
|     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);
 | |
|             }
 | |
|         );
 | |
|     }
 | |
| }
 |