2020-07-31 01:45:54 +02:00
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" ;
2020-07-31 16:17:16 +02:00
import { FilteredLayer } from "./Logic/FilteredLayer" ;
2020-10-19 12:08:42 +02:00
import { UpdateFromOverpass } from "./Logic/UpdateFromOverpass" ;
2020-08-17 17:23:15 +02:00
import { UIEventSource } from "./Logic/UIEventSource" ;
import { LocalStorageSource } from "./Logic/Web/LocalStorageSource" ;
import { QueryParameters } from "./Logic/Web/QueryParameters" ;
2020-09-28 00:30:39 +02:00
import { BaseLayer } from "./Logic/BaseLayer" ;
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-07-31 01:45:54 +02:00
/ * *
* Contains the global state : a bunch of UI - event sources
* /
2020-10-02 19:00:24 +02:00
export default class State {
2020-07-31 01:45:54 +02:00
// The singleton of the global state
public static state : State ;
2020-11-17 02:22:48 +01:00
2020-11-17 16:29:51 +01:00
public static vNumber = "0.1.3-rc5" ;
2020-11-17 02:22:48 +01:00
2020-08-22 18:57:27 +02:00
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {
2020-10-14 12:15:45 +02:00
addNewPointsUnlock : 0 ,
2020-09-05 01:40:43 +02:00
moreScreenUnlock : 5 ,
2020-09-03 16:44:48 +02:00
personalLayoutUnlock : 20 ,
tagsVisibleAt : 100 ,
2020-09-05 01:40:43 +02:00
mapCompleteHelpUnlock : 200 ,
2020-09-03 16:44:48 +02:00
tagsVisibleAndWikiLinked : 150 ,
themeGeneratorReadOnlyUnlock : 200 ,
2020-09-05 01:40:43 +02:00
themeGeneratorFullUnlock : 500 ,
2020-09-27 21:00:37 +02:00
addNewPointWithUnreadMessagesUnlock : 500 ,
minZoomLevelToAddNewPoints : ( Utils . isRetina ( ) ? 18 : 19 )
2020-08-22 18:57:27 +02:00
} ;
2020-07-31 01:45:54 +02:00
2020-11-11 16:23:49 +01:00
public static runningFromConsole : boolean = false ;
2020-07-31 01:45:54 +02:00
2020-11-11 16:23:49 +01:00
public readonly layoutToUse = new UIEventSource < LayoutConfig > ( undefined ) ;
2020-07-31 01:45:54 +02:00
/ * *
The mapping from id - > UIEventSource < properties >
* /
public allElements : ElementStorage ;
/ * *
THe change handler
* /
public changes : Changes ;
/ * *
THe basemap with leaflet instance
* /
2020-07-31 17:38:03 +02:00
public bm ;
2020-09-28 00:30:39 +02:00
/ * *
* Background layer id
* /
public availableBackgroundLayers : UIEventSource < BaseLayer [ ] > ;
2020-07-31 01:45:54 +02:00
/ * *
2020-08-22 16:00:33 +02:00
The user credentials
2020-07-31 01:45:54 +02:00
* /
public osmConnection : OsmConnection ;
2020-08-26 15:36:04 +02:00
public favouriteLayers : UIEventSource < string [ ] > ;
2020-10-19 12:08:42 +02:00
public layerUpdater : UpdateFromOverpass ;
2020-08-26 15:36:04 +02:00
2020-07-31 16:17:16 +02:00
public filteredLayers : UIEventSource < FilteredLayer [ ] > = new UIEventSource < FilteredLayer [ ] > ( [ ] )
2020-09-25 21:58:29 +02:00
2020-07-31 01:45:54 +02: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
* /
2020-11-15 01:16:35 +01:00
public readonly selectedElement = new UIEventSource < any > ( undefined ) ;
2020-07-31 01:45:54 +02:00
2020-09-18 00:31:54 +02:00
public readonly zoom : UIEventSource < number > ;
public readonly lat : UIEventSource < number > ;
public readonly lon : UIEventSource < number > ;
2020-07-31 01:45:54 +02:00
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 > ;
2020-08-08 14:43:48 +02:00
public readonly featureSwitchGeolocation : UIEventSource < boolean > ;
2020-07-31 01:45:54 +02:00
/ * *
* The map location : currently centered lat , lon and zoom
* /
public readonly locationControl = new UIEventSource < { lat : number , lon : number , zoom : number } > ( undefined ) ;
/ * *
* The location as delivered by the GPS
* /
public currentGPSLocation : UIEventSource < {
2020-11-05 12:28:02 +01:00
latlng : { lat :number , lng :number } ,
2020-07-31 01:45:54 +02:00
accuracy : number
2020-11-05 12:28:02 +01:00
} > = new UIEventSource < { latlng : { lat :number , lng :number } , accuracy : number } > ( undefined ) ;
2020-08-26 15:36:04 +02:00
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
2020-11-13 23:58:11 +01:00
public layerControlIsOpened : UIEventSource < boolean > = QueryParameters . GetQueryParameter ( "layer-control-toggle" , "false" , "Wether or not the layer control is shown" )
2020-09-17 23:53:57 +02:00
. map < boolean > ( ( str ) = > str !== "false" , [ ] , b = > "" + b )
2020-11-13 23:58:11 +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 > ${ State . userJourney . mapCompleteHelpUnlock } changesets) ` ) . map < number > (
2020-09-17 23:53:57 +02:00
str = > isNaN ( Number ( str ) ) ? 0 : Number ( str ) , [ ] , n = > "" + n
2020-08-30 01:13:18 +02:00
) ;
2020-07-31 01:45:54 +02:00
2020-11-11 16:23:49 +01:00
constructor ( layoutToUse : LayoutConfig ) {
2020-08-26 15:36:04 +02:00
const self = this ;
2020-09-18 00:31:54 +02:00
this . layoutToUse . setData ( layoutToUse ) ;
function 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 ;
}
2020-09-27 22:48:43 +02:00
return ( "" + fl ) . substr ( 0 , 8 ) ;
2020-09-18 00:31:54 +02:00
} )
}
2020-09-18 12:00:38 +02:00
this . zoom = asFloat (
2020-11-13 23:58:11 +01:00
QueryParameters . GetQueryParameter ( "z" , "" + layoutToUse . startZoom , "The initial/current zoom level" )
2020-10-17 00:42:35 +02:00
. syncWith ( LocalStorageSource . Get ( "zoom" ) ) ) ;
2020-11-13 23:58:11 +01:00
this . lat = asFloat ( QueryParameters . GetQueryParameter ( "lat" , "" + layoutToUse . startLat , "The initial/current latitude" )
2020-10-17 00:42:35 +02:00
. syncWith ( LocalStorageSource . Get ( "lat" ) ) ) ;
2020-11-13 23:58:11 +01:00
this . lon = asFloat ( QueryParameters . GetQueryParameter ( "lon" , "" + layoutToUse . startLon , "The initial/current longitude of the app" )
2020-10-17 00:42:35 +02:00
. syncWith ( LocalStorageSource . Get ( "lon" ) ) ) ;
2020-09-18 00:31:54 +02:00
2020-07-31 01:45:54 +02:00
this . locationControl = new UIEventSource < { lat : number , lon : number , zoom : number } > ( {
2020-08-26 15:36:04 +02:00
zoom : Utils.asFloat ( this . zoom . data ) ,
lat : Utils.asFloat ( this . lat . data ) ,
lon : Utils.asFloat ( this . lon . data ) ,
2020-07-31 01:45:54 +02:00
} ) . addCallback ( ( latlonz ) = > {
2020-09-18 00:31:54 +02:00
this . zoom . setData ( latlonz . zoom ) ;
this . lat . setData ( latlonz . lat ) ;
this . lon . setData ( latlonz . lon ) ;
2020-08-26 15:36:04 +02:00
} ) ;
2020-07-31 01:45:54 +02:00
2020-08-26 15:36:04 +02:00
this . layoutToUse . addCallback ( layoutToUse = > {
const lcd = self . locationControl . data ;
2020-11-11 16:23:49 +01:00
lcd . zoom = lcd . zoom ? ? layoutToUse ? . startZoom ;
2020-08-26 15:36:04 +02:00
lcd . lat = lcd . lat ? ? layoutToUse ? . startLat ;
lcd . lon = lcd . lon ? ? layoutToUse ? . startLon ;
self . locationControl . ping ( ) ;
} ) ;
2020-07-31 01:45:54 +02:00
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 ) ;
2020-07-31 01:45:54 +02:00
// I'm so sorry about someone trying to decipher this
2020-08-26 15:36:04 +02:00
2020-07-31 01:45:54 +02:00
// It takes the current layout, extracts the default value for this query paramter. A query parameter event source is then retreived and flattened
return UIEventSource . flatten (
2020-08-26 15:36:04 +02:00
self . layoutToUse . map ( ( layout ) = > {
const defaultValue = deflt ( layout ) ;
2020-11-12 12:18:02 +01:00
const queryParam = QueryParameters . GetQueryParameter ( key , "" + defaultValue , documentation )
2020-08-26 15:36:04 +02:00
return queryParam . map ( ( str ) = > str === undefined ? defaultValue : ( str !== "false" ) ) ;
} ) , [ queryParameterSource ] ) ;
2020-07-31 01:45:54 +02:00
}
2020-11-12 12:18:02 +01:00
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 ,
"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" ) ;
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 (
2020-08-27 11:11:20 +02:00
testParam === "true" ,
2020-11-13 23:58:11 +01:00
QueryParameters . GetQueryParameter ( "oauth_token" , undefined ,
"Used to complete the login" ) ,
2020-08-30 01:13:18 +02:00
layoutToUse . id ,
true
2020-07-31 04:58:58 +02:00
) ;
2020-08-26 15:36:04 +02:00
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-11 16:23:49 +01:00
this . installedThemes = this . osmConnection . preferencesHandler . preferences . map < { layout : LayoutConfig , definition : string } [ ] > ( allPreferences = > {
const installedThemes : { layout : LayoutConfig , definition : string } [ ] = [ ] ;
2020-08-26 20:11:43 +02:00
if ( allPreferences === undefined ) {
return installedThemes ;
}
2020-10-18 00:28:51 +02:00
const invalidThemes = [ ]
2020-08-26 20:11:43 +02:00
for ( const allPreferencesKey in allPreferences ) {
const themename = allPreferencesKey . match ( /^mapcomplete-installed-theme-(.*)-combined-length$/ ) ;
if ( themename && themename [ 1 ] !== "" ) {
2020-10-10 14:09:12 +02:00
const customLayout = self . osmConnection . GetLongPreference ( "installed-theme-" + themename [ 1 ] ) ;
2020-11-11 16:23:49 +01:00
if ( customLayout . data === undefined ) {
2020-08-26 20:11:43 +02:00
console . log ( "No data defined for " , themename [ 1 ] ) ;
continue ;
}
try {
2020-11-12 12:18:02 +01:00
const json = btoa ( customLayout . data ) ;
console . log ( json ) ;
2020-11-11 16:23:49 +01:00
const layout = new LayoutConfig (
2020-11-12 12:18:02 +01:00
JSON . parse ( json ) ) ;
2020-08-26 20:11:43 +02:00
installedThemes . push ( {
2020-08-31 02:59:47 +02:00
layout : layout ,
2020-08-26 20:11:43 +02:00
definition : customLayout.data
} ) ;
} catch ( e ) {
2020-10-18 00:28:51 +02:00
console . warn ( "Could not parse custom layout from preferences - deleting: " , allPreferencesKey , e , customLayout . data ) ;
invalidThemes . push ( themename [ 1 ] )
2020-08-26 20:11:43 +02:00
}
}
}
2020-10-18 00:28:51 +02:00
for ( const invalid of invalidThemes ) {
console . error ( "Attempting to remove " , invalid )
this . osmConnection . GetLongPreference (
"installed-theme-" + invalid
) . setData ( null ) ;
}
2020-08-26 20:11:43 +02:00
return installedThemes ;
} ) ;
2020-09-15 14:00:31 +02:00
// IMportant: the favourite layers are initiliazed _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 ) = > {
2020-08-26 15:36:04 +02:00
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 ( )
2020-08-26 15:36:04 +02:00
this . layoutToUse . map ( ( layoutToUse ) = > {
2020-09-03 03:16:43 +02:00
return Translations . WT ( layoutToUse ? . title ) ? . txt ? ? "MapComplete"
2020-08-26 15:36:04 +02:00
} , [ Locale . language ]
2020-09-03 03:16:43 +02:00
) . addCallbackAndRun ( ( title ) = > {
2020-08-26 15:36:04 +02:00
document . title = title
} ) ;
2020-07-31 04:58:58 +02:00
this . allElements = new ElementStorage ( ) ;
2020-08-27 11:11:20 +02:00
this . changes = new Changes ( ) ;
2020-07-31 04:58:58 +02:00
2020-08-26 15:36:04 +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
2020-07-31 16:17:16 +02:00
}