mapcomplete/State.ts

280 lines
12 KiB
TypeScript
Raw Normal View History

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";
2020-07-31 04:58:58 +02:00
import Locale from "./UI/i18n/Locale";
import Translations from "./UI/i18n/Translations";
import {UIEventSource} from "./Logic/UIEventSource";
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
import {QueryParameters} from "./Logic/Web/QueryParameters";
2020-11-11 16:23:49 +01:00
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
2020-11-17 02:22:48 +01:00
import Hash from "./Logic/Web/Hash";
2020-12-08 23:44:34 +01:00
import {MangroveIdentity} from "./Logic/Web/MangroveReviews";
2021-01-03 00:19:42 +01:00
import InstalledThemes from "./Logic/Actors/InstalledThemes";
import BaseLayer from "./Models/BaseLayer";
import Loc from "./Models/Loc";
import Constants from "./Models/Constants";
2021-01-03 13:50:18 +01:00
import UpdateFromOverpass from "./Logic/Actors/UpdateFromOverpass";
import LayerConfig from "./Customizations/JSON/LayerConfig";
/**
* Contains the global state: a bunch of UI-event sources
*/
export default class State {
// The singleton of the global state
public static state: State;
2020-11-11 16:23:49 +01:00
public static runningFromConsole: boolean = false;
2020-11-11 16:23:49 +01:00
public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined);
/**
The mapping from id -> UIEventSource<properties>
*/
public allElements: ElementStorage;
/**
THe change handler
*/
public changes: Changes;
/**
2021-01-02 21:03:40 +01:00
The leaflet instance of the big basemap
*/
2021-01-02 21:03:40 +01:00
public leafletMap = new UIEventSource<L.Map>(undefined);
/**
* Background layer id
*/
public availableBackgroundLayers: UIEventSource<BaseLayer[]>;
/**
2020-08-22 16:00:33 +02:00
The user credentials
*/
public osmConnection: OsmConnection;
2020-12-08 23:44:34 +01:00
public mangroveIdentity: MangroveIdentity;
public favouriteLayers: UIEventSource<string[]>;
2020-10-19 12:08:42 +02:00
public layerUpdater: UpdateFromOverpass;
public filteredLayers: UIEventSource<{
readonly isDisplayed: UIEventSource<boolean>,
readonly layerDef: LayerConfig;
}[]> = new UIEventSource<{
readonly isDisplayed: UIEventSource<boolean>,
readonly layerDef: LayerConfig;
}[]>([])
2020-12-08 23:44:34 +01:00
/**
* The message that should be shown at the center of the screen
*/
public readonly centerMessage = new UIEventSource<string>("");
/**
This message is shown full screen on mobile devices
*/
public readonly fullScreenMessage = new UIEventSource<UIElement>(undefined);
/**
The latest element that was selected - used to generate the right UI at the right place
*/
public readonly selectedElement = new UIEventSource<any>(undefined);
public readonly featureSwitchUserbadge: UIEventSource<boolean>;
public readonly featureSwitchSearch: UIEventSource<boolean>;
public readonly featureSwitchLayers: UIEventSource<boolean>;
public readonly featureSwitchAddNew: UIEventSource<boolean>;
public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>;
public readonly featureSwitchIframe: UIEventSource<boolean>;
2020-08-07 00:45:33 +02:00
public readonly featureSwitchMoreQuests: UIEventSource<boolean>;
public readonly featureSwitchShareScreen: UIEventSource<boolean>;
public readonly featureSwitchGeolocation: UIEventSource<boolean>;
/**
* The map location: currently centered lat, lon and zoom
*/
public readonly locationControl = new UIEventSource<Loc>(undefined);
2021-01-03 13:50:18 +01:00
public backgroundLayer;
2021-01-02 21:03:40 +01:00
/* 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;
2020-11-11 16:23:49 +01:00
public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>;
2020-07-31 04:58:58 +02:00
public layerControlIsOpened: UIEventSource<boolean> =
QueryParameters.GetQueryParameter("layer-control-toggle", "false", "Whether or not the layer control is shown")
.map<boolean>((str) => str !== "false", [], b => "" + b)
2021-01-02 19:09:49 +01:00
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
);
2020-11-11 16:23:49 +01:00
constructor(layoutToUse: LayoutConfig) {
const self = this;
this.layoutToUse.setData(layoutToUse);
2021-01-02 21:03:40 +01:00
const zoom = State.asFloat(
2020-11-13 23:58:11 +01:00
QueryParameters.GetQueryParameter("z", "" + layoutToUse.startZoom, "The initial/current zoom level")
.syncWith(LocalStorageSource.Get("zoom")));
2021-01-02 21:03:40 +01:00
const lat = State.asFloat(QueryParameters.GetQueryParameter("lat", "" + layoutToUse.startLat, "The initial/current latitude")
.syncWith(LocalStorageSource.Get("lat")));
2021-01-02 21:03:40 +01:00
const lon = State.asFloat(QueryParameters.GetQueryParameter("lon", "" + layoutToUse.startLon, "The initial/current longitude of the app")
.syncWith(LocalStorageSource.Get("lon")));
this.locationControl = new UIEventSource<Loc>({
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;
2020-11-11 16:23:49 +01:00
lcd.zoom = lcd.zoom ?? layoutToUse?.startZoom;
lcd.lat = lcd.lat ?? layoutToUse?.startLat;
lcd.lon = lcd.lon ?? layoutToUse?.startLon;
self.locationControl.ping();
});
2020-11-13 23:58:11 +01:00
function featSw(key: string, deflt: (layout: LayoutConfig) => boolean, documentation: string): UIEventSource<boolean> {
const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined, documentation);
// I'm so sorry about someone trying to decipher this
2021-01-02 21:03:40 +01:00
// 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]);
}
this.featureSwitchUserbadge = featSw("fs-userbadge", (layoutToUse) => layoutToUse?.enableUserBadge ?? true,
2020-11-13 23:58:11 +01:00
"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,
2020-11-13 23:58:11 +01:00
"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");
2021-01-02 21:03:40 +01:00
2020-11-13 23:58:11 +01:00
const testParam = 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").data;
2020-07-31 04:58:58 +02:00
this.osmConnection = new OsmConnection(
testParam === "true",
2020-11-13 23:58:11 +01:00
QueryParameters.GetQueryParameter("oauth_token", undefined,
"Used to complete the login"),
layoutToUse.id,
true
2020-07-31 04:58:58 +02:00
);
2020-12-08 23:44:34 +01:00
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove")
);
2020-11-17 02:22:48 +01:00
const h = Hash.Get();
this.selectedElement.addCallback(selected => {
if (selected === undefined) {
h.setData("");
} else {
h.setData(selected.id)
}
}
)
h.addCallbackAndRun(hash => {
if (hash === undefined || hash === "") {
self.selectedElement.setData(undefined);
2020-11-17 02:22:48 +01:00
}
})
2021-01-03 00:19:42 +01:00
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 = this.osmConnection.GetLongPreference("favouriteLayers").map(
str => Utils.Dedup(str?.split(";")) ?? [],
[], layers => Utils.Dedup(layers)?.join(";")
);
2020-07-31 04:58:58 +02:00
Locale.language.syncWith(this.osmConnection.GetPreference("language"));
Locale.language.addCallback((currentLanguage) => {
const layoutToUse = self.layoutToUse.data;
if (layoutToUse === undefined) {
return;
}
2020-11-11 16:23:49 +01:00
if (this.layoutToUse.data.language.indexOf(currentLanguage) < 0) {
console.log("Resetting language to", layoutToUse.language[0], "as", currentLanguage, " is unsupported")
2020-07-31 04:58:58 +02:00
// The current language is not supported -> switch to a supported one
2020-11-11 16:23:49 +01:00
Locale.language.setData(layoutToUse.language[0]);
2020-07-31 04:58:58 +02:00
}
}).ping()
this.layoutToUse.map((layoutToUse) => {
return Translations.WT(layoutToUse?.title)?.txt ?? "MapComplete"
}, [Locale.language]
).addCallbackAndRun((title) => {
document.title = title
});
2020-07-31 04:58:58 +02:00
this.allElements = new ElementStorage();
this.changes = new Changes();
2020-07-31 04:58:58 +02:00
if (State.runningFromConsole) {
2020-07-31 17:11:44 +02:00
console.warn("running from console - not initializing map. Assuming test.html");
return;
}
2020-07-31 04:58:58 +02:00
if (document.getElementById("leafletDiv") === null) {
console.warn("leafletDiv not found - not initializing map. Assuming test.html");
return;
}
2020-07-31 16:17:16 +02:00
}
2020-09-10 19:33:06 +02:00
private static asFloat(source: UIEventSource<string>): UIEventSource<number> {
2021-01-02 21:03:40 +01:00
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);
})
}
2020-07-31 16:17:16 +02:00
}