2021-07-29 10:37:23 +02:00
import { FixedUiElement } from "./UI/Base/FixedUiElement" ;
2021-06-10 01:36:20 +02:00
import Toggle from "./UI/Input/Toggle" ;
2020-10-02 19:00:24 +02:00
import State from "./State" ;
2021-07-29 10:37:23 +02:00
import { UIEventSource } from "./Logic/UIEventSource" ;
import { QueryParameters } from "./Logic/Web/QueryParameters" ;
2021-01-04 04:06:21 +01:00
import StrayClickHandler from "./Logic/Actors/StrayClickHandler" ;
import SimpleAddUI from "./UI/BigComponents/SimpleAddUI" ;
import CenterMessageBox from "./UI/CenterMessageBox" ;
import UserBadge from "./UI/BigComponents/UserBadge" ;
import SearchAndGo from "./UI/BigComponents/SearchAndGo" ;
2021-07-29 10:37:23 +02:00
import { LocalStorageSource } from "./Logic/Web/LocalStorageSource" ;
import { Utils } from "./Utils" ;
2020-11-06 01:58:26 +01:00
import Svg from "./Svg" ;
import Link from "./UI/Base/Link" ;
2021-07-28 14:50:22 +02:00
import * as personal from "./assets/themes/personal/personal.json" ;
2020-11-17 02:22:48 +01:00
import * as L from "leaflet" ;
2021-01-03 00:19:42 +01:00
import Img from "./UI/Base/Img" ;
2021-01-04 04:06:21 +01:00
import Attribution from "./UI/BigComponents/Attribution" ;
2021-09-21 02:10:42 +02:00
import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter" ;
2021-01-04 04:06:21 +01:00
import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs" ;
2021-09-21 02:10:42 +02:00
import ShowDataLayer from "./UI/ShowDataLayer/ShowDataLayer" ;
2021-01-06 02:09:04 +01:00
import Hash from "./Logic/Web/Hash" ;
2021-01-15 00:29:07 +01:00
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline" ;
2021-02-14 19:45:02 +01:00
import ScrollableFullScreen from "./UI/Base/ScrollableFullScreen" ;
import Translations from "./UI/i18n/Translations" ;
2021-02-21 03:38:12 +01:00
import MapControlButton from "./UI/MapControlButton" ;
2021-04-04 03:22:56 +02:00
import LZString from "lz-string" ;
2021-05-17 00:18:21 +02:00
import AllKnownLayers from "./Customizations/AllKnownLayers" ;
2021-06-15 00:28:59 +02:00
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers" ;
2021-07-29 10:37:23 +02:00
import { TagsFilter } from "./Logic/Tags/TagsFilter" ;
2021-07-28 16:48:59 +02:00
import LeftControls from "./UI/BigComponents/LeftControls" ;
import RightControls from "./UI/BigComponents/RightControls" ;
2021-08-07 23:11:34 +02:00
import { LayoutConfigJson } from "./Models/ThemeConfig/Json/LayoutConfigJson" ;
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig" ;
import LayerConfig from "./Models/ThemeConfig/LayerConfig" ;
2021-09-03 13:48:04 +02:00
import Minimap from "./UI/Base/Minimap" ;
2021-09-21 03:10:15 +02:00
import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler" ;
2021-09-22 17:29:50 +02:00
import Combine from "./UI/Base/Combine" ;
import { SubtleButton } from "./UI/Base/SubtleButton" ;
2021-09-26 17:36:39 +02:00
import ShowTileInfo from "./UI/ShowDataLayer/ShowTileInfo" ;
import { Tiles } from "./Models/TileRange" ;
import PerTileCountAggregator from "./UI/ShowDataLayer/PerTileCountAggregator" ;
2020-07-29 15:05:19 +02:00
export class InitUiElements {
2021-07-27 20:41:06 +02:00
static InitAll (
layoutToUse : LayoutConfig ,
layoutFromBase64 : string ,
testing : UIEventSource < string > ,
layoutName : string ,
layoutDefinition : string = ""
) {
if ( layoutToUse === undefined ) {
console . log ( "Incorrect layout" ) ;
new FixedUiElement (
` Error: incorrect layout <i> ${ layoutName } </i><br/><a href='https:// ${ window . location . host } /'>Go back</a> `
)
. AttachTo ( "centermessage" )
2021-07-29 10:37:23 +02:00
. onClick ( ( ) = > {
} ) ;
2021-07-27 20:41:06 +02:00
throw "Incorrect layout" ;
}
2021-07-23 15:56:22 +02:00
2021-07-27 20:41:06 +02:00
console . log (
"Using layout: " ,
layoutToUse . id ,
"LayoutFromBase64 is " ,
layoutFromBase64
) ;
2021-07-23 15:56:22 +02:00
2021-07-27 20:41:06 +02:00
State . state = new State ( layoutToUse ) ;
2021-07-23 15:56:22 +02:00
2021-07-27 20:41:06 +02:00
// This 'leaks' the global state via the window object, useful for debugging
// @ts-ignore
window . mapcomplete_state = State . state ;
2020-09-07 02:25:45 +02:00
2021-07-27 20:41:06 +02:00
if ( layoutToUse . hideFromOverview ) {
State . state . osmConnection
. GetPreference ( "hidden-theme-" + layoutToUse . id + "-enabled" )
. setData ( "true" ) ;
}
2021-07-23 15:56:22 +02:00
2021-07-27 20:41:06 +02:00
if ( layoutFromBase64 !== "false" ) {
State . state . layoutDefinition = layoutDefinition ;
console . log (
"Layout definition:" ,
Utils . EllipsesAfter ( State . state . layoutDefinition , 100 )
) ;
if ( testing . data !== "true" ) {
State . state . osmConnection . OnLoggedIn ( ( ) = > {
State . state . osmConnection
. GetLongPreference ( "installed-theme-" + layoutToUse . id )
. setData ( State . state . layoutDefinition ) ;
} ) ;
} else {
console . warn (
"NOT saving custom layout to OSM as we are tesing -> probably in an iFrame"
) ;
}
}
function updateFavs() {
// This is purely for the personal theme to load the layers there
const favs = State . state . favouriteLayers . data ? ? [ ] ;
const neededLayers = new Set < LayerConfig > ( ) ;
console . log ( "Favourites are: " , favs ) ;
layoutToUse . layers . splice ( 0 , layoutToUse . layers . length ) ;
let somethingChanged = false ;
for ( const fav of favs ) {
if ( AllKnownLayers . sharedLayers . has ( fav ) ) {
const layer = AllKnownLayers . sharedLayers . get ( fav ) ;
if ( ! neededLayers . has ( layer ) ) {
neededLayers . add ( layer ) ;
somethingChanged = true ;
}
}
for ( const layouts of State . state . installedThemes . data ) {
for ( const layer of layouts . layout . layers ) {
if ( typeof layer === "string" ) {
continue ;
}
if ( layer . id === fav ) {
if ( ! neededLayers . has ( layer ) ) {
neededLayers . add ( layer ) ;
somethingChanged = true ;
}
}
}
}
}
if ( somethingChanged ) {
State . state . layoutToUse . data . layers = Array . from ( neededLayers ) ;
State . state . layoutToUse . ping ( ) ;
2021-09-21 02:10:42 +02:00
State . state . featurePipeline ? . ForceRefresh ( ) ;
2021-07-27 20:41:06 +02:00
}
}
if ( layoutToUse . customCss !== undefined ) {
Utils . LoadCustomCss ( layoutToUse . customCss ) ;
}
InitUiElements . InitBaseMap ( ) ;
InitUiElements . OnlyIf ( State . state . featureSwitchUserbadge , ( ) = > {
new UserBadge ( ) . AttachTo ( "userbadge" ) ;
} ) ;
InitUiElements . OnlyIf ( State . state . featureSwitchSearch , ( ) = > {
new SearchAndGo ( ) . AttachTo ( "searchbox" ) ;
2021-07-23 15:56:22 +02:00
} ) ;
2021-07-27 20:41:06 +02:00
InitUiElements . OnlyIf ( State . state . featureSwitchWelcomeMessage , ( ) = > {
InitUiElements . InitWelcomeMessage ( ) ;
} ) ;
2021-07-28 14:50:22 +02:00
if ( State . state . featureSwitchIframe . data ) {
2021-07-27 20:41:06 +02:00
const currentLocation = State . state . locationControl ;
2021-07-28 14:50:22 +02:00
const url = ` ${ window . location . origin } ${
window . location . pathname
} ? z = $ { currentLocation . data . zoom ? ? 0 } & lat = $ {
currentLocation . data . lat ? ? 0
} & lon = $ { currentLocation . data . lon ? ? 0 } ` ;
2021-07-27 20:41:06 +02:00
new MapControlButton (
new Link ( Svg . pop_out_img , url , true ) . SetClass (
"block w-full h-full p-1.5"
)
) . AttachTo ( "messagesbox" ) ;
}
2021-09-26 17:36:39 +02:00
function addHomeMarker() {
const userDetails = State . state . osmConnection . userDetails . data ;
if ( userDetails === undefined ) {
return false ;
}
console . log ( "Adding home location of " , userDetails )
const home = userDetails . home ;
if ( home === undefined ) {
return userDetails . loggedIn ; // If logged in, the home is not set and we unregister. If not logged in, we stay registered if a login still comes
}
const leaflet = State . state . leafletMap . data ;
if ( leaflet === undefined ) {
return false ;
}
const color = getComputedStyle ( document . body ) . getPropertyValue (
"--subtle-detail-color"
) ;
const icon = L . icon ( {
iconUrl : Img.AsData (
Svg . home_white_bg . replace ( /#ffffff/g , color )
) ,
iconSize : [ 30 , 30 ] ,
iconAnchor : [ 15 , 15 ] ,
2021-07-27 20:41:06 +02:00
} ) ;
2021-09-26 17:36:39 +02:00
const marker = L . marker ( [ home . lat , home . lon ] , { icon : icon } ) ;
marker . addTo ( leaflet ) ;
return true ;
}
State . state . osmConnection . userDetails
. addCallbackAndRunD ( _ = > addHomeMarker ( ) ) ;
State . state . leafletMap . addCallbackAndRunD ( _ = > addHomeMarker ( ) )
2021-07-27 20:41:06 +02:00
if ( layoutToUse . id === personal . id ) {
updateFavs ( ) ;
}
2021-07-28 16:48:59 +02:00
2021-07-27 20:41:06 +02:00
InitUiElements . setupAllLayerElements ( ) ;
if ( layoutToUse . id === personal . id ) {
State . state . favouriteLayers . addCallback ( updateFavs ) ;
State . state . installedThemes . addCallback ( updateFavs ) ;
} else {
State . state . locationControl . ping ( ) ;
2021-07-26 12:26:41 +02:00
}
2020-09-07 02:25:45 +02:00
2021-09-21 03:10:15 +02:00
new SelectedFeatureHandler ( Hash . hash , State . state )
2021-07-27 20:41:06 +02:00
// Reset the loading message once things are loaded
new CenterMessageBox ( ) . AttachTo ( "centermessage" ) ;
document
. getElementById ( "centermessage" )
. classList . add ( "pointer-events-none" ) ;
}
static LoadLayoutFromHash (
userLayoutParam : UIEventSource < string >
) : [ LayoutConfig , string ] {
2021-09-26 17:36:39 +02:00
let hash = location . hash . substr ( 1 ) ;
2021-09-22 17:29:50 +02:00
try {
2021-07-27 20:41:06 +02:00
const layoutFromBase64 = userLayoutParam . data ;
// layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
const dedicatedHashFromLocalStorage = LocalStorageSource . Get (
"user-layout-" + layoutFromBase64 . replace ( " " , "_" )
) ;
if ( dedicatedHashFromLocalStorage . data ? . length < 10 ) {
dedicatedHashFromLocalStorage . setData ( undefined ) ;
2020-09-07 02:25:45 +02:00
}
2021-07-27 20:41:06 +02:00
const hashFromLocalStorage = LocalStorageSource . Get (
"last-loaded-user-layout"
) ;
if ( hash . length < 10 ) {
2021-07-28 14:50:22 +02:00
hash =
dedicatedHashFromLocalStorage . data ? ?
hashFromLocalStorage . data ;
2021-07-27 20:41:06 +02:00
} else {
console . log ( "Saving hash to local storage" ) ;
hashFromLocalStorage . setData ( hash ) ;
dedicatedHashFromLocalStorage . setData ( hash ) ;
2021-04-04 03:22:56 +02:00
}
2021-07-27 20:41:06 +02:00
let json : { } ;
try {
json = JSON . parse ( atob ( hash ) ) ;
} catch ( e ) {
// We try to decode with lz-string
json = JSON . parse (
Utils . UnMinify ( LZString . decompressFromBase64 ( hash ) )
) as LayoutConfigJson ;
}
// @ts-ignore
const layoutToUse = new LayoutConfig ( json , false ) ;
userLayoutParam . setData ( layoutToUse . id ) ;
return [ layoutToUse , btoa ( Utils . MinifyJSON ( JSON . stringify ( json ) ) ) ] ;
} catch ( e ) {
2021-09-26 17:36:39 +02:00
if ( hash === undefined || hash . length < 10 ) {
2021-09-22 17:29:50 +02:00
e = "Did you effectively add a theme? It seems no data could be found."
}
2021-09-26 17:36:39 +02:00
2021-09-22 17:29:50 +02:00
new Combine ( [
"Error: could not parse the custom layout:" ,
2021-09-26 17:36:39 +02:00
new FixedUiElement ( "" + e ) . SetClass ( "alert" ) ,
new SubtleButton ( "./assets/svg/mapcomplete_logo.svg" ,
"Go back to the theme overview" ,
{ url : window.location.protocol + "//" + window . location . hostname + "/index.html" , newTab : false } )
2021-09-22 17:29:50 +02:00
] )
. SetClass ( "flex flex-col" )
. AttachTo ( "centermessage" ) ;
2021-07-27 20:41:06 +02:00
throw e ;
2020-09-07 02:25:45 +02:00
}
}
2021-07-27 20:41:06 +02:00
private static OnlyIf (
featureSwitch : UIEventSource < boolean > ,
callback : ( ) = > void
) {
featureSwitch . addCallbackAndRun ( ( ) = > {
if ( featureSwitch . data ) {
callback ( ) ;
}
} ) ;
2020-07-30 00:59:08 +02:00
}
2020-07-31 04:58:58 +02:00
2021-07-27 20:41:06 +02:00
private static InitWelcomeMessage() {
const isOpened = new UIEventSource < boolean > ( false ) ;
const fullOptions = new FullWelcomePaneWithTabs ( isOpened ) ;
2021-07-19 16:23:13 +02:00
2021-07-27 20:41:06 +02:00
// ?-Button on Desktop, opens panel with close-X.
const help = new MapControlButton ( Svg . help_svg ( ) ) ;
help . onClick ( ( ) = > isOpened . setData ( true ) ) ;
2021-07-28 14:50:22 +02:00
new Toggle (
2021-09-06 21:56:58 +02:00
fullOptions . SetClass ( "welcomeMessage pointer-events-auto" ) ,
help . SetClass ( "pointer-events-auto" ) ,
2021-07-28 14:50:22 +02:00
isOpened
2021-09-06 21:56:58 +02:00
)
. AttachTo ( "messagesbox" ) ;
2021-07-27 20:41:06 +02:00
const openedTime = new Date ( ) . getTime ( ) ;
State . state . locationControl . addCallback ( ( ) = > {
if ( new Date ( ) . getTime ( ) - openedTime < 15 * 1000 ) {
// Don't autoclose the first 15 secs when the map is moving
return ;
}
isOpened . setData ( false ) ;
} ) ;
2021-07-19 16:23:13 +02:00
2021-07-27 20:41:06 +02:00
State . state . selectedElement . addCallbackAndRunD ( ( _ ) = > {
isOpened . setData ( false ) ;
} ) ;
isOpened . setData (
Hash . hash . data === undefined ||
2021-07-29 10:37:23 +02:00
Hash . hash . data === "" ||
Hash . hash . data == "welcome"
2021-07-27 20:41:06 +02:00
) ;
}
2021-09-03 13:48:04 +02:00
2021-07-27 20:41:06 +02:00
private static InitBaseMap() {
State . state . availableBackgroundLayers =
AvailableBaseLayers . AvailableLayersAt ( State . state . locationControl ) ;
State . state . backgroundLayer = State . state . backgroundLayerId . map (
( selectedId : string ) = > {
if ( selectedId === undefined ) {
return AvailableBaseLayers . osmCarto ;
}
const available = State . state . availableBackgroundLayers . data ;
for ( const layer of available ) {
if ( layer . id === selectedId ) {
return layer ;
}
}
return AvailableBaseLayers . osmCarto ;
} ,
[ State . state . availableBackgroundLayers ] ,
( layer ) = > layer . id
) ;
2021-07-23 15:56:22 +02:00
2021-09-21 02:10:42 +02:00
new BackgroundLayerResetter (
2021-07-27 20:41:06 +02:00
State . state . backgroundLayer ,
State . state . locationControl ,
State . state . availableBackgroundLayers ,
State . state . layoutToUse . map (
( layout : LayoutConfig ) = > layout . defaultBackgroundId
)
2021-07-26 12:26:41 +02:00
) ;
2021-07-27 20:41:06 +02:00
const attr = new Attribution (
State . state . locationControl ,
State . state . osmConnection . userDetails ,
State . state . layoutToUse ,
2021-09-21 02:10:42 +02:00
State . state . currentBounds
2021-07-26 12:26:41 +02:00
) ;
2021-07-27 20:41:06 +02:00
2021-09-21 02:10:42 +02:00
Minimap . createMiniMap ( {
2021-09-03 13:48:04 +02:00
background : State.state.backgroundLayer ,
location : State.state.locationControl ,
leafletMap : State.state.leafletMap ,
2021-09-21 02:10:42 +02:00
bounds : State.state.currentBounds ,
2021-09-03 13:48:04 +02:00
attribution : attr ,
lastClickLocation : State.state.LastClickLocation
} ) . SetClass ( "w-full h-full" )
. AttachTo ( "leafletDiv" )
2021-07-27 20:41:06 +02:00
const layout = State . state . layoutToUse . data ;
if ( layout . lockLocation ) {
if ( layout . lockLocation === true ) {
2021-09-26 17:36:39 +02:00
const tile = Tiles . embedded_tile (
2021-07-27 20:41:06 +02:00
layout . startLat ,
layout . startLon ,
layout . startZoom - 1
) ;
2021-09-26 17:36:39 +02:00
const bounds = Tiles . tile_bounds ( tile . z , tile . x , tile . y ) ;
2021-07-27 20:41:06 +02:00
// We use the bounds to get a sense of distance for this zoom level
const latDiff = bounds [ 0 ] [ 0 ] - bounds [ 1 ] [ 0 ] ;
const lonDiff = bounds [ 0 ] [ 1 ] - bounds [ 1 ] [ 1 ] ;
layout . lockLocation = [
[ layout . startLat - latDiff , layout . startLon - lonDiff ] ,
[ layout . startLat + latDiff , layout . startLon + lonDiff ] ,
] ;
}
console . warn ( "Locking the bounds to " , layout . lockLocation ) ;
2021-09-03 13:48:04 +02:00
State . state . leafletMap . addCallbackAndRunD ( map = > {
// @ts-ignore
map . setMaxBounds ( layout . lockLocation ) ;
map . setMinZoom ( layout . startZoom ) ;
} )
2021-07-26 12:26:41 +02:00
}
2021-07-27 20:41:06 +02:00
}
2021-09-21 02:10:42 +02:00
private static InitLayers ( ) : void {
2021-07-27 20:41:06 +02:00
const state = State . state ;
state . filteredLayers = state . layoutToUse . map ( ( layoutToUse ) = > {
const flayers = [ ] ;
for ( const layer of layoutToUse . layers ) {
const isDisplayed = QueryParameters . GetQueryParameter (
"layer-" + layer . id ,
"true" ,
"Wether or not layer " + layer . id + " is shown"
) . map < boolean > (
( str ) = > str !== "false" ,
[ ] ,
( b ) = > b . toString ( )
) ;
const flayer = {
isDisplayed : isDisplayed ,
layerDef : layer ,
2021-09-26 17:36:39 +02:00
isSufficientlyZoomed : state.locationControl.map ( l = > {
return l . zoom >= ( layer . minzoomVisible ? ? layer . minzoom )
} ) ,
2021-07-28 14:50:22 +02:00
appliedFilters : new UIEventSource < TagsFilter > ( undefined ) ,
2021-07-27 20:41:06 +02:00
} ;
flayers . push ( flayer ) ;
}
return flayers ;
} ) ;
2021-09-26 17:36:39 +02:00
const clusterCounter = new PerTileCountAggregator ( State . state . locationControl . map ( l = > {
const z = l . zoom + 1
if ( z < 7 ) {
return 7
}
return z
} ) )
const clusterShow = Math . min ( . . . State . state . layoutToUse . data . layers . map ( layer = > layer . minzoomVisible ? ? layer . minzoom ) )
new ShowDataLayer ( {
features : clusterCounter ,
leafletMap : State.state.leafletMap ,
layerToShow : ShowTileInfo.styling ,
doShowLayer : State.state.locationControl.map ( l = > l . zoom < clusterShow )
} )
2021-09-21 02:10:42 +02:00
State . state . featurePipeline = new FeaturePipeline (
source = > {
2021-09-26 17:36:39 +02:00
const clustering = State . state . layoutToUse . data . clustering
const doShowFeatures = source . features . map (
f = > {
const z = State . state . locationControl . data . zoom
if ( z >= clustering . maxZoom ) {
return true
}
if ( z < source . layer . layerDef . minzoom ) {
return false ;
}
if ( f . length > clustering . minNeededElements ) {
console . log ( "Activating clustering for tile " , Tiles . tile_from_index ( source . tileIndex ) , " as it has " , f . length , "features (clustering starts at)" , clustering . minNeededElements )
return false
}
return true
} , [ State . state . locationControl ]
)
clusterCounter . addTile ( source , doShowFeatures . map ( b = > ! b ) )
/ *
new ShowTileInfo ( { source : source ,
leafletMap : State.state.leafletMap ,
layer : source.layer.layerDef ,
doShowLayer : doShowFeatures.map ( b = > ! b )
} ) * /
2021-09-21 02:10:42 +02:00
new ShowDataLayer (
{
features : source ,
leafletMap : State.state.leafletMap ,
2021-09-26 17:36:39 +02:00
layerToShow : source.layer.layerDef ,
doShowLayer : doShowFeatures
2021-09-21 02:10:42 +02:00
}
) ;
} , state
2021-07-27 20:41:06 +02:00
) ;
}
private static setupAllLayerElements() {
// ------------- Setup the layers -------------------------------
2021-09-21 02:10:42 +02:00
InitUiElements . InitLayers ( ) ;
2021-07-28 16:48:59 +02:00
2021-09-21 02:10:42 +02:00
new LeftControls ( State . state ) . AttachTo ( "bottom-left" ) ;
2021-07-28 16:48:59 +02:00
new RightControls ( ) . AttachTo ( "bottom-right" ) ;
2021-07-27 20:41:06 +02:00
// ------------------ Setup various other UI elements ------------
InitUiElements . OnlyIf ( State . state . featureSwitchAddNew , ( ) = > {
let presetCount = 0 ;
for ( const layer of State . state . filteredLayers . data ) {
for ( const preset of layer . layerDef . presets ) {
presetCount ++ ;
}
}
if ( presetCount == 0 ) {
return ;
}
const newPointDialogIsShown = new UIEventSource < boolean > ( false ) ;
const addNewPoint = new ScrollableFullScreen (
( ) = > Translations . t . general . add . title . Clone ( ) ,
( ) = > new SimpleAddUI ( newPointDialogIsShown ) ,
"new" ,
newPointDialogIsShown
) ;
addNewPoint . isShown . addCallback ( ( isShown ) = > {
if ( ! isShown ) {
State . state . LastClickLocation . setData ( undefined ) ;
}
} ) ;
new StrayClickHandler (
State . state . LastClickLocation ,
State . state . selectedElement ,
State . state . filteredLayers ,
State . state . leafletMap ,
addNewPoint
) ;
} ) ;
}
2021-07-19 16:23:13 +02:00
}