forked from MapComplete/MapComplete
		
	Huge refactoring of state and initial UI setup
This commit is contained in:
		
							parent
							
								
									4e43673de5
								
							
						
					
					
						commit
						eff6b5bfad
					
				
					 37 changed files with 5232 additions and 4907 deletions
				
			
		|  | @ -1,567 +0,0 @@ | ||||||
| import {FixedUiElement} from "./UI/Base/FixedUiElement"; |  | ||||||
| import Toggle from "./UI/Input/Toggle"; |  | ||||||
| import State from "./State"; |  | ||||||
| import {UIEventSource} from "./Logic/UIEventSource"; |  | ||||||
| import {QueryParameters} from "./Logic/Web/QueryParameters"; |  | ||||||
| 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"; |  | ||||||
| import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; |  | ||||||
| import {Utils} from "./Utils"; |  | ||||||
| import Svg from "./Svg"; |  | ||||||
| import Link from "./UI/Base/Link"; |  | ||||||
| import * as personal from "./assets/themes/personal/personal.json"; |  | ||||||
| import * as L from "leaflet"; |  | ||||||
| import Img from "./UI/Base/Img"; |  | ||||||
| import Attribution from "./UI/BigComponents/Attribution"; |  | ||||||
| import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter"; |  | ||||||
| import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs"; |  | ||||||
| import ShowDataLayer from "./UI/ShowDataLayer/ShowDataLayer"; |  | ||||||
| import Hash from "./Logic/Web/Hash"; |  | ||||||
| import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; |  | ||||||
| import ScrollableFullScreen from "./UI/Base/ScrollableFullScreen"; |  | ||||||
| import Translations from "./UI/i18n/Translations"; |  | ||||||
| import MapControlButton from "./UI/MapControlButton"; |  | ||||||
| import LZString from "lz-string"; |  | ||||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; |  | ||||||
| import LeftControls from "./UI/BigComponents/LeftControls"; |  | ||||||
| import RightControls from "./UI/BigComponents/RightControls"; |  | ||||||
| import {LayoutConfigJson} from "./Models/ThemeConfig/Json/LayoutConfigJson"; |  | ||||||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; |  | ||||||
| import Minimap from "./UI/Base/Minimap"; |  | ||||||
| import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; |  | ||||||
| import Combine from "./UI/Base/Combine"; |  | ||||||
| import {SubtleButton} from "./UI/Base/SubtleButton"; |  | ||||||
| import ShowTileInfo from "./UI/ShowDataLayer/ShowTileInfo"; |  | ||||||
| import {Tiles} from "./Models/TileRange"; |  | ||||||
| import {TileHierarchyAggregator} from "./UI/ShowDataLayer/TileHierarchyAggregator"; |  | ||||||
| import FilterConfig from "./Models/ThemeConfig/FilterConfig"; |  | ||||||
| import FilteredLayer from "./Models/FilteredLayer"; |  | ||||||
| import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; |  | ||||||
| import ShowOverlayLayer from "./UI/ShowDataLayer/ShowOverlayLayer"; |  | ||||||
| 
 |  | ||||||
| export class InitUiElements { |  | ||||||
|     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") |  | ||||||
|                 .onClick(() => { |  | ||||||
|                 }); |  | ||||||
|             throw "Incorrect layout"; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         console.log( |  | ||||||
|             "Using layout: ", |  | ||||||
|             layoutToUse.id, |  | ||||||
|             "LayoutFromBase64 is ", |  | ||||||
|             layoutFromBase64 |  | ||||||
|         ); |  | ||||||
|          |  | ||||||
|         if(layoutToUse.id === personal.id){ |  | ||||||
|             layoutToUse.layers = AllKnownLayouts.AllPublicLayers() |  | ||||||
|             for (const layer of layoutToUse.layers) { |  | ||||||
|                 layer.minzoomVisible = Math.max(layer.minzoomVisible, layer.minzoom) |  | ||||||
|                 layer.minzoom = Math.max(16, layer.minzoom) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         State.state = new State(layoutToUse); |  | ||||||
| 
 |  | ||||||
|         if(layoutToUse.id === personal.id) { |  | ||||||
|             // Disable overpass all together
 |  | ||||||
|             State.state.overpassMaxZoom.setData(0) |  | ||||||
|              |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|             // This 'leaks' the global state via the window object, useful for debugging
 |  | ||||||
|         // @ts-ignore
 |  | ||||||
|         window.mapcomplete_state = State.state; |  | ||||||
| 
 |  | ||||||
|         if (layoutToUse.hideFromOverview) { |  | ||||||
|             State.state.osmConnection |  | ||||||
|                 .GetPreference("hidden-theme-" + layoutToUse.id + "-enabled") |  | ||||||
|                 .setData("true"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         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" |  | ||||||
|                 ); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         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"); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         InitUiElements.OnlyIf(State.state.featureSwitchWelcomeMessage, () => { |  | ||||||
|             InitUiElements.InitWelcomeMessage(); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         if (State.state.featureSwitchIframe.data) { |  | ||||||
|             const currentLocation = State.state.locationControl; |  | ||||||
|             const url = `${window.location.origin}${ |  | ||||||
|                 window.location.pathname |  | ||||||
|             }?z=${currentLocation.data.zoom ?? 0}&lat=${ |  | ||||||
|                 currentLocation.data.lat ?? 0 |  | ||||||
|             }&lon=${currentLocation.data.lon ?? 0}`;
 |  | ||||||
|             new MapControlButton( |  | ||||||
|                 new Link(Svg.pop_out_img, url, true).SetClass( |  | ||||||
|                     "block w-full h-full p-1.5" |  | ||||||
|                 ) |  | ||||||
|             ).AttachTo("messagesbox"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         function addHomeMarker() { |  | ||||||
|             const userDetails = State.state.osmConnection.userDetails.data; |  | ||||||
|             if (userDetails === undefined) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|             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], |  | ||||||
|             }); |  | ||||||
|             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()) |  | ||||||
|     |  | ||||||
|         InitUiElements.setupAllLayerElements(); |  | ||||||
|         State.state.locationControl.ping(); |  | ||||||
| 
 |  | ||||||
|         new SelectedFeatureHandler(Hash.hash, State.state) |  | ||||||
| 
 |  | ||||||
|         // 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] { |  | ||||||
|         let hash = location.hash.substr(1); |  | ||||||
|         try { |  | ||||||
|             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); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             const hashFromLocalStorage = LocalStorageSource.Get( |  | ||||||
|                 "last-loaded-user-layout" |  | ||||||
|             ); |  | ||||||
|             if (hash.length < 10) { |  | ||||||
|                 hash = |  | ||||||
|                     dedicatedHashFromLocalStorage.data ?? |  | ||||||
|                     hashFromLocalStorage.data; |  | ||||||
|             } else { |  | ||||||
|                 console.log("Saving hash to local storage"); |  | ||||||
|                 hashFromLocalStorage.setData(hash); |  | ||||||
|                 dedicatedHashFromLocalStorage.setData(hash); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             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) { |  | ||||||
| 
 |  | ||||||
|             if (hash === undefined || hash.length < 10) { |  | ||||||
|                 e = "Did you effectively add a theme? It seems no data could be found." |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             new Combine([ |  | ||||||
|                 "Error: could not parse the custom layout:", |  | ||||||
|                 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}) |  | ||||||
| 
 |  | ||||||
|             ]) |  | ||||||
|                 .SetClass("flex flex-col") |  | ||||||
|                 .AttachTo("centermessage"); |  | ||||||
|             throw e; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static OnlyIf( |  | ||||||
|         featureSwitch: UIEventSource<boolean>, |  | ||||||
|         callback: () => void |  | ||||||
|     ) { |  | ||||||
|         featureSwitch.addCallbackAndRun(() => { |  | ||||||
|             if (featureSwitch.data) { |  | ||||||
|                 callback(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static InitWelcomeMessage() { |  | ||||||
|         const isOpened = new UIEventSource<boolean>(false); |  | ||||||
|         const fullOptions = new FullWelcomePaneWithTabs(isOpened); |  | ||||||
| 
 |  | ||||||
|         // ?-Button on Desktop, opens panel with close-X.
 |  | ||||||
|         const help = new MapControlButton(Svg.help_svg()); |  | ||||||
|         help.onClick(() => isOpened.setData(true)); |  | ||||||
|         new Toggle( |  | ||||||
|             fullOptions.SetClass("welcomeMessage pointer-events-auto"), |  | ||||||
|             help.SetClass("pointer-events-auto"), |  | ||||||
|             isOpened |  | ||||||
|         ) |  | ||||||
|             .AttachTo("messagesbox"); |  | ||||||
|         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); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         State.state.selectedElement.addCallbackAndRunD((_) => { |  | ||||||
|             isOpened.setData(false); |  | ||||||
|         }); |  | ||||||
|         isOpened.setData( |  | ||||||
|             Hash.hash.data === undefined || |  | ||||||
|             Hash.hash.data === "" || |  | ||||||
|             Hash.hash.data == "welcome" |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     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 |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         new BackgroundLayerResetter( |  | ||||||
|             State.state.backgroundLayer, |  | ||||||
|             State.state.locationControl, |  | ||||||
|             State.state.availableBackgroundLayers, |  | ||||||
|             State.state.layoutToUse.defaultBackgroundId |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         const attr = new Attribution( |  | ||||||
|             State.state.locationControl, |  | ||||||
|             State.state.osmConnection.userDetails, |  | ||||||
|             State.state.layoutToUse, |  | ||||||
|             State.state.currentBounds |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         Minimap.createMiniMap({ |  | ||||||
|             background: State.state.backgroundLayer, |  | ||||||
|             location: State.state.locationControl, |  | ||||||
|             leafletMap: State.state.leafletMap, |  | ||||||
|             bounds: State.state.currentBounds, |  | ||||||
|             attribution: attr, |  | ||||||
|             lastClickLocation: State.state.LastClickLocation |  | ||||||
|         }).SetClass("w-full h-full") |  | ||||||
|             .AttachTo("leafletDiv") |  | ||||||
| 
 |  | ||||||
|         const layout = State.state.layoutToUse; |  | ||||||
|         if (layout.lockLocation) { |  | ||||||
|             if (layout.lockLocation === true) { |  | ||||||
|                 const tile = Tiles.embedded_tile( |  | ||||||
|                     layout.startLat, |  | ||||||
|                     layout.startLon, |  | ||||||
|                     layout.startZoom - 1 |  | ||||||
|                 ); |  | ||||||
|                 const bounds = Tiles.tile_bounds(tile.z, tile.x, tile.y); |  | ||||||
|                 // 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); |  | ||||||
|             State.state.leafletMap.addCallbackAndRunD(map => { |  | ||||||
|                 // @ts-ignore
 |  | ||||||
|                 map.setMaxBounds(layout.lockLocation); |  | ||||||
|                 map.setMinZoom(layout.startZoom); |  | ||||||
|             }) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static InitLayers(): void { |  | ||||||
|         const state = State.state; |  | ||||||
|         const empty = [] |  | ||||||
| 
 |  | ||||||
|         const flayers: FilteredLayer[] = []; |  | ||||||
| 
 |  | ||||||
|         for (const layer of state.layoutToUse.layers) { |  | ||||||
|             let defaultShown = "true" |  | ||||||
|             if(state.layoutToUse.id === personal.id){ |  | ||||||
|                 defaultShown = "false" |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             let isDisplayed: UIEventSource<boolean> |  | ||||||
|             if(state.layoutToUse.id === personal.id){ |  | ||||||
|                 isDisplayed = State.state.osmConnection.GetPreference("personal-theme-layer-" + layer.id + "-enabled") |  | ||||||
|                     .map(value => value === "yes", [], enabled => { |  | ||||||
|                         return enabled ? "yes" : ""; |  | ||||||
|                     }) |  | ||||||
|                 isDisplayed.addCallbackAndRun(d =>console.log("IsDisplayed for layer", layer.id, "is currently", d) ) |  | ||||||
|             }else{ |  | ||||||
|                 isDisplayed = QueryParameters.GetQueryParameter( |  | ||||||
|                     "layer-" + layer.id, |  | ||||||
|                     defaultShown, |  | ||||||
|                     "Wether or not layer " + layer.id + " is shown" |  | ||||||
|                 ).map<boolean>( |  | ||||||
|                     (str) => str !== "false", |  | ||||||
|                     [], |  | ||||||
|                     (b) => b.toString() |  | ||||||
|                 ); |  | ||||||
|             } |  | ||||||
|             const flayer = { |  | ||||||
|                 isDisplayed: isDisplayed, |  | ||||||
|                 layerDef: layer, |  | ||||||
|                 appliedFilters: new UIEventSource<{ filter: FilterConfig, selected: number }[]>([]), |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             if (layer.filters.length > 0) { |  | ||||||
|                 const filtersPerName = new Map<string, FilterConfig>() |  | ||||||
|                 layer.filters.forEach(f => filtersPerName.set(f.id, f)) |  | ||||||
|                 const qp = QueryParameters.GetQueryParameter("filter-" + layer.id, "","Filtering state for a layer") |  | ||||||
|                 flayer.appliedFilters.map(filters => { |  | ||||||
|                     filters = filters ?? [] |  | ||||||
|                     return filters.map(f => f.filter.id + "." + f.selected).join(",") |  | ||||||
|                 }, [], textual => { |  | ||||||
|                     if(textual.length === 0){ |  | ||||||
|                         return empty |  | ||||||
|                     } |  | ||||||
|                     return textual.split(",").map(part => { |  | ||||||
|                         const [filterId, selected] = part.split("."); |  | ||||||
|                         return {filter: filtersPerName.get(filterId), selected: Number(selected)} |  | ||||||
|                     }).filter(f => f.filter !== undefined && !isNaN(f.selected)) |  | ||||||
|                 }).syncWith(qp, true) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             flayers.push(flayer); |  | ||||||
|         } |  | ||||||
|         state.filteredLayers = new UIEventSource<FilteredLayer[]>(flayers); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         const clustering = State.state.layoutToUse.clustering |  | ||||||
|         const clusterCounter = TileHierarchyAggregator.createHierarchy() |  | ||||||
|         new ShowDataLayer({ |  | ||||||
|             features: clusterCounter.getCountsForZoom(clustering, State.state.locationControl, State.state.layoutToUse.clustering.minNeededElements), |  | ||||||
|             leafletMap: State.state.leafletMap, |  | ||||||
|             layerToShow: ShowTileInfo.styling, |  | ||||||
|             enablePopups: false |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         State.state.featurePipeline = new FeaturePipeline( |  | ||||||
|             source => { |  | ||||||
| 
 |  | ||||||
|                 clusterCounter.addTile(source) |  | ||||||
| 
 |  | ||||||
|                 // Do show features indicates if the 'showDataLayer' should be shown
 |  | ||||||
|                 const doShowFeatures = source.features.map( |  | ||||||
|                     f => { |  | ||||||
|                         const z = State.state.locationControl.data.zoom |  | ||||||
|                          |  | ||||||
|                         if(!source.layer.isDisplayed.data){ |  | ||||||
|                             return false; |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         const bounds = State.state.currentBounds.data |  | ||||||
|                         if(bounds === undefined){ |  | ||||||
|                             // Map is not yet displayed
 |  | ||||||
|                             return false; |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         if (!source.bbox.overlapsWith(bounds)) { |  | ||||||
|                             // Not within range -> features are hidden
 |  | ||||||
|                             return false |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                         if (z < source.layer.layerDef.minzoom) { |  | ||||||
|                             // Layer is always hidden for this zoom level
 |  | ||||||
|                             return false; |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         if (z > clustering.maxZoom) { |  | ||||||
|                             return true |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         if (f.length > clustering.minNeededElements) { |  | ||||||
|                             // This tile alone already has too much features
 |  | ||||||
|                             return false |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex); |  | ||||||
|                         if (tileZ >= z) { |  | ||||||
| 
 |  | ||||||
|                             while (tileZ > z) { |  | ||||||
|                                 tileZ-- |  | ||||||
|                                 tileX = Math.floor(tileX / 2) |  | ||||||
|                                 tileY = Math.floor(tileY / 2) |  | ||||||
|                             } |  | ||||||
| 
 |  | ||||||
|                             if (clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))?.totalValue > clustering.minNeededElements) { |  | ||||||
|                                 // To much elements
 |  | ||||||
|                                 return false |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                        |  | ||||||
|                         return true |  | ||||||
|                     }, [State.state.currentBounds, source.layer.isDisplayed] |  | ||||||
|                 ) |  | ||||||
| 
 |  | ||||||
|                 new ShowDataLayer( |  | ||||||
|                     { |  | ||||||
|                         features: source, |  | ||||||
|                         leafletMap: State.state.leafletMap, |  | ||||||
|                         layerToShow: source.layer.layerDef, |  | ||||||
|                         doShowLayer: doShowFeatures |  | ||||||
|                     } |  | ||||||
|                 ); |  | ||||||
|             }, state |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         const initialized =new Set() |  | ||||||
|         for (const overlayToggle of State.state.overlayToggles) { |  | ||||||
|             new ShowOverlayLayer(overlayToggle.config, state.leafletMap, overlayToggle.isDisplayed) |  | ||||||
|             initialized.add(overlayToggle.config) |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         for (const tileLayerSource of state.layoutToUse.tileLayerSources) { |  | ||||||
|             if (initialized.has(tileLayerSource)) { |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
|             new ShowOverlayLayer(tileLayerSource, state.leafletMap) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static setupAllLayerElements() { |  | ||||||
|         // ------------- Setup the layers -------------------------------
 |  | ||||||
| 
 |  | ||||||
|         InitUiElements.InitLayers(); |  | ||||||
| 
 |  | ||||||
|         new LeftControls(State.state).AttachTo("bottom-left"); |  | ||||||
|         new RightControls().AttachTo("bottom-right"); |  | ||||||
| 
 |  | ||||||
|         // ------------------ 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 |  | ||||||
|             ); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										168
									
								
								Logic/DetermineLayout.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								Logic/DetermineLayout.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,168 @@ | ||||||
|  | import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import {QueryParameters} from "./Web/QueryParameters"; | ||||||
|  | import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; | ||||||
|  | import {FixedUiElement} from "../UI/Base/FixedUiElement"; | ||||||
|  | import {Utils} from "../Utils"; | ||||||
|  | import Combine from "../UI/Base/Combine"; | ||||||
|  | import {SubtleButton} from "../UI/Base/SubtleButton"; | ||||||
|  | import BaseUIElement from "../UI/BaseUIElement"; | ||||||
|  | import {UIEventSource} from "./UIEventSource"; | ||||||
|  | import {LocalStorageSource} from "./Web/LocalStorageSource"; | ||||||
|  | import LZString from "lz-string"; | ||||||
|  | import * as personal from "../assets/themes/personal/personal.json"; | ||||||
|  | 
 | ||||||
|  | export default class DetermineLayout { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets the correct layout for this website | ||||||
|  |      */ | ||||||
|  |     public static async GetLayout(): Promise<[LayoutConfig, string]> { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const loadCustomThemeParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme") | ||||||
|  |         const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data); | ||||||
|  | 
 | ||||||
|  |         if (layoutFromBase64.startsWith("http")) { | ||||||
|  |             // The userLayout is actually an url
 | ||||||
|  |             const layout = await DetermineLayout.LoadRemoteTheme(layoutFromBase64) | ||||||
|  |             return [layout, undefined] | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (layoutFromBase64 !== "false") { | ||||||
|  |             // We have to load something from the hash (or from disk)
 | ||||||
|  |             let loaded = DetermineLayout.LoadLayoutFromHash(loadCustomThemeParam); | ||||||
|  |             if (loaded === null) { | ||||||
|  |                 return [null, undefined] | ||||||
|  |             } | ||||||
|  |             return loaded | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let layoutId: string = undefined | ||||||
|  |         if (location.href.indexOf("buurtnatuur.be") >= 0) { | ||||||
|  |             layoutId = "buurtnatuur" | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const path = window.location.pathname.split("/").slice(-1)[0]; | ||||||
|  |         if (path !== "index.html" && path !== "") { | ||||||
|  |             layoutId = path; | ||||||
|  |             if (path.endsWith(".html")) { | ||||||
|  |                 layoutId = path.substr(0, path.length - 5); | ||||||
|  |             } | ||||||
|  |             console.log("Using layout", layoutId); | ||||||
|  |         } | ||||||
|  |         layoutId = QueryParameters.GetQueryParameter("layout", layoutId, "The layout to load into MapComplete").data; | ||||||
|  |         const layoutToUse: LayoutConfig = AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase()); | ||||||
|  | 
 | ||||||
|  |         if (layoutToUse?.id === personal.id) { | ||||||
|  |             layoutToUse.layers = AllKnownLayouts.AllPublicLayers() | ||||||
|  |             for (const layer of layoutToUse.layers) { | ||||||
|  |                 layer.minzoomVisible = Math.max(layer.minzoomVisible, layer.minzoom) | ||||||
|  |                 layer.minzoom = Math.max(16, layer.minzoom) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         return [layoutToUse, undefined] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> { | ||||||
|  |         console.log("Downloading map theme from ", link); | ||||||
|  | 
 | ||||||
|  |         new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`) | ||||||
|  |             .AttachTo("centermessage"); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  | 
 | ||||||
|  |             const data = await Utils.downloadJson(link) | ||||||
|  |             try { | ||||||
|  |                 let parsed = data; | ||||||
|  |                 if (typeof parsed == "string") { | ||||||
|  |                     parsed = JSON.parse(parsed); | ||||||
|  |                 } | ||||||
|  |                 // Overwrite the id to the url
 | ||||||
|  |                 parsed.id = link; | ||||||
|  |                 return new LayoutConfig(parsed, false).patchImages(link, data); | ||||||
|  |             } catch (e) { | ||||||
|  | 
 | ||||||
|  |                 DetermineLayout.ShowErrorOnCustomTheme( | ||||||
|  |                     `<a href="${link}">${link}</a> is invalid:`, | ||||||
|  |                     new FixedUiElement(e) | ||||||
|  |                 ) | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } catch (e) { | ||||||
|  |             DetermineLayout.ShowErrorOnCustomTheme( | ||||||
|  |                 `<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`, | ||||||
|  |                 new FixedUiElement(e) | ||||||
|  |             ) | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static LoadLayoutFromHash( | ||||||
|  |         userLayoutParam: UIEventSource<string> | ||||||
|  |     ): [LayoutConfig, string] | null { | ||||||
|  |         let hash = location.hash.substr(1); | ||||||
|  |         try { | ||||||
|  |             // layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
 | ||||||
|  |             const dedicatedHashFromLocalStorage = LocalStorageSource.Get( | ||||||
|  |                 "user-layout-" + userLayoutParam.data.replace(" ", "_") | ||||||
|  |             ); | ||||||
|  |             if (dedicatedHashFromLocalStorage.data?.length < 10) { | ||||||
|  |                 dedicatedHashFromLocalStorage.setData(undefined); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const hashFromLocalStorage = LocalStorageSource.Get( | ||||||
|  |                 "last-loaded-user-layout" | ||||||
|  |             ); | ||||||
|  |             if (hash.length < 10) { | ||||||
|  |                 hash = | ||||||
|  |                     dedicatedHashFromLocalStorage.data ?? | ||||||
|  |                     hashFromLocalStorage.data; | ||||||
|  |             } else { | ||||||
|  |                 console.log("Saving hash to local storage"); | ||||||
|  |                 hashFromLocalStorage.setData(hash); | ||||||
|  |                 dedicatedHashFromLocalStorage.setData(hash); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             let json: any; | ||||||
|  |             try { | ||||||
|  |                 json = JSON.parse(atob(hash)); | ||||||
|  |             } catch (e) { | ||||||
|  |                 // We try to decode with lz-string
 | ||||||
|  |                 try { | ||||||
|  |                     json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash))) | ||||||
|  |                 } catch (e) { | ||||||
|  |                     DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON")) | ||||||
|  |                     return null; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const layoutToUse = new LayoutConfig(json, false); | ||||||
|  |             userLayoutParam.setData(layoutToUse.id); | ||||||
|  |             return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; | ||||||
|  |         } catch (e) { | ||||||
|  |             if (hash === undefined || hash.length < 10) { | ||||||
|  |                 DetermineLayout.ShowErrorOnCustomTheme("Could not load a theme from the hash", new FixedUiElement("Hash does not contain data")) | ||||||
|  |             } | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static ShowErrorOnCustomTheme( | ||||||
|  |         intro: string = "Error: could not parse the custom layout:", | ||||||
|  |         error: BaseUIElement) { | ||||||
|  |         new Combine([ | ||||||
|  |             intro, | ||||||
|  |             error.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}) | ||||||
|  | 
 | ||||||
|  |         ]) | ||||||
|  |             .SetClass("flex flex-col clickable") | ||||||
|  |             .AttachTo("centermessage"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -73,7 +73,7 @@ export default class FeaturePipeline { | ||||||
|             readonly overpassTimeout: UIEventSource<number>; |             readonly overpassTimeout: UIEventSource<number>; | ||||||
|             readonly overpassMaxZoom: UIEventSource<number>; |             readonly overpassMaxZoom: UIEventSource<number>; | ||||||
|             readonly osmConnection: OsmConnection |             readonly osmConnection: OsmConnection | ||||||
|             readonly currentBounds: UIEventSource<BBox> |             readonly currentBounds: UIEventSource<BBox>, | ||||||
|         }) { |         }) { | ||||||
|         this.state = state; |         this.state = state; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,5 @@ | ||||||
| 
 |  | ||||||
| import State from "../../../State"; |  | ||||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | import FilteredLayer from "../../../Models/FilteredLayer"; | ||||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||||
| import {Utils} from "../../../Utils"; |  | ||||||
| import {UIEventSource} from "../../UIEventSource"; | import {UIEventSource} from "../../UIEventSource"; | ||||||
| import Loc from "../../../Models/Loc"; | import Loc from "../../../Models/Loc"; | ||||||
| import TileHierarchy from "./TileHierarchy"; | import TileHierarchy from "./TileHierarchy"; | ||||||
|  | @ -25,7 +22,6 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | ||||||
|             leafletMap: any |             leafletMap: any | ||||||
|         } |         } | ||||||
|     ) { |     ) { | ||||||
|         state = State.state |  | ||||||
|         const self = this; |         const self = this; | ||||||
| 
 | 
 | ||||||
|         this.loadedTiles = new Map<number,FeatureSourceForLayer & Tiled>() |         this.loadedTiles = new Map<number,FeatureSourceForLayer & Tiled>() | ||||||
|  |  | ||||||
							
								
								
									
										93
									
								
								Logic/State/ElementsState.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								Logic/State/ElementsState.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,93 @@ | ||||||
|  | import FeatureSwitchState from "./FeatureSwitchState"; | ||||||
|  | import {ElementStorage} from "../ElementStorage"; | ||||||
|  | import {Changes} from "../Osm/Changes"; | ||||||
|  | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import {UIEventSource} from "../UIEventSource"; | ||||||
|  | import Loc from "../../Models/Loc"; | ||||||
|  | import {BBox} from "../BBox"; | ||||||
|  | import {QueryParameters} from "../Web/QueryParameters"; | ||||||
|  | import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||||
|  | import {Utils} from "../../Utils"; | ||||||
|  | import ChangeToElementsActor from "../Actors/ChangeToElementsActor"; | ||||||
|  | import PendingChangesUploader from "../Actors/PendingChangesUploader"; | ||||||
|  | import TitleHandler from "../Actors/TitleHandler"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * The part of the state keeping track of where the elements, loading them, configuring the feature pipeline etc | ||||||
|  |  */ | ||||||
|  | export default class ElementsState extends FeatureSwitchState{ | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      The mapping from id -> UIEventSource<properties> | ||||||
|  |      */ | ||||||
|  |     public allElements: ElementStorage = new ElementStorage(); | ||||||
|  |     /** | ||||||
|  |      THe change handler | ||||||
|  |      */ | ||||||
|  |     public changes: Changes = new Changes(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      The latest element that was selected | ||||||
|  |      */ | ||||||
|  |     public readonly selectedElement = new UIEventSource<any>( | ||||||
|  |         undefined, | ||||||
|  |         "Selected element" | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 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) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     constructor(layoutToUse: LayoutConfig) { | ||||||
|  |         super(layoutToUse); | ||||||
|  |         { | ||||||
|  |             // -- Location control initialization
 | ||||||
|  |             const zoom = UIEventSource.asFloat( | ||||||
|  |                 QueryParameters.GetQueryParameter( | ||||||
|  |                     "z", | ||||||
|  |                     "" + (layoutToUse?.startZoom ?? 1), | ||||||
|  |                     "The initial/current zoom level" | ||||||
|  |                 ).syncWith(LocalStorageSource.Get("zoom")) | ||||||
|  |             ); | ||||||
|  |             const lat = UIEventSource.asFloat( | ||||||
|  |                 QueryParameters.GetQueryParameter( | ||||||
|  |                     "lat", | ||||||
|  |                     "" + (layoutToUse?.startLat ?? 0), | ||||||
|  |                     "The initial/current latitude" | ||||||
|  |                 ).syncWith(LocalStorageSource.Get("lat")) | ||||||
|  |             ); | ||||||
|  |             const lon = UIEventSource.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); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         new ChangeToElementsActor(this.changes, this.allElements) | ||||||
|  |         new PendingChangesUploader(this.changes, this.selectedElement); | ||||||
|  |         new TitleHandler(this); | ||||||
|  |      | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										172
									
								
								Logic/State/FeaturePipelineState.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								Logic/State/FeaturePipelineState.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,172 @@ | ||||||
|  | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import FeaturePipeline from "../FeatureSource/FeaturePipeline"; | ||||||
|  | import State from "../../State"; | ||||||
|  | import {Tiles} from "../../Models/TileRange"; | ||||||
|  | import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer"; | ||||||
|  | import {TileHierarchyAggregator} from "../../UI/ShowDataLayer/TileHierarchyAggregator"; | ||||||
|  | import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo"; | ||||||
|  | import {UIEventSource} from "../UIEventSource"; | ||||||
|  | import MapState from "./MapState"; | ||||||
|  | import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"; | ||||||
|  | import Hash from "../Web/Hash"; | ||||||
|  | import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"; | ||||||
|  | import Translations from "../../UI/i18n/Translations"; | ||||||
|  | import SimpleAddUI from "../../UI/BigComponents/SimpleAddUI"; | ||||||
|  | import StrayClickHandler from "../Actors/StrayClickHandler"; | ||||||
|  | 
 | ||||||
|  | export default class FeaturePipelineState extends MapState { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The piece of code which fetches data from various sources and shows it on the background map | ||||||
|  |      */ | ||||||
|  |     public readonly featurePipeline: FeaturePipeline; | ||||||
|  |     private readonly featureAggregator: TileHierarchyAggregator; | ||||||
|  | 
 | ||||||
|  |     constructor(layoutToUse: LayoutConfig) { | ||||||
|  |         super(layoutToUse); | ||||||
|  | 
 | ||||||
|  |         const clustering = layoutToUse.clustering | ||||||
|  |         this.featureAggregator = TileHierarchyAggregator.createHierarchy(); | ||||||
|  |         const clusterCounter = this.featureAggregator | ||||||
|  |         const self = this; | ||||||
|  |         this.featurePipeline = new FeaturePipeline( | ||||||
|  |             source => { | ||||||
|  | 
 | ||||||
|  |                 clusterCounter.addTile(source) | ||||||
|  | 
 | ||||||
|  |                 // Do show features indicates if the 'showDataLayer' should be shown
 | ||||||
|  |                 const doShowFeatures = source.features.map( | ||||||
|  |                     f => { | ||||||
|  |                         const z = State.state.locationControl.data.zoom | ||||||
|  | 
 | ||||||
|  |                         if (!source.layer.isDisplayed.data) { | ||||||
|  |                             return false; | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         const bounds = State.state.currentBounds.data | ||||||
|  |                         if (bounds === undefined) { | ||||||
|  |                             // Map is not yet displayed
 | ||||||
|  |                             return false; | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         if (!source.bbox.overlapsWith(bounds)) { | ||||||
|  |                             // Not within range -> features are hidden
 | ||||||
|  |                             return false | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |                         if (z < source.layer.layerDef.minzoom) { | ||||||
|  |                             // Layer is always hidden for this zoom level
 | ||||||
|  |                             return false; | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         if (z > clustering.maxZoom) { | ||||||
|  |                             return true | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         if (f.length > clustering.minNeededElements) { | ||||||
|  |                             // This tile alone already has too much features
 | ||||||
|  |                             return false | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex); | ||||||
|  |                         if (tileZ >= z) { | ||||||
|  | 
 | ||||||
|  |                             while (tileZ > z) { | ||||||
|  |                                 tileZ-- | ||||||
|  |                                 tileX = Math.floor(tileX / 2) | ||||||
|  |                                 tileY = Math.floor(tileY / 2) | ||||||
|  |                             } | ||||||
|  | 
 | ||||||
|  |                             if (clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))?.totalValue > clustering.minNeededElements) { | ||||||
|  |                                 // To much elements
 | ||||||
|  |                                 return false | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |                         return true | ||||||
|  |                     }, [State.state.currentBounds, source.layer.isDisplayed] | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |                 new ShowDataLayer( | ||||||
|  |                     { | ||||||
|  |                         features: source, | ||||||
|  |                         leafletMap: self.leafletMap, | ||||||
|  |                         layerToShow: source.layer.layerDef, | ||||||
|  |                         doShowLayer: doShowFeatures, | ||||||
|  |                         allElements: self.allElements, | ||||||
|  |                         selectedElement: self.selectedElement | ||||||
|  |                     } | ||||||
|  |                 ); | ||||||
|  |             }, this | ||||||
|  |         ); | ||||||
|  |         new SelectedFeatureHandler(Hash.hash, this) | ||||||
|  |          | ||||||
|  |         this.AddClusteringToMap(this.leafletMap) | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Adds the cluster-tiles to the given map | ||||||
|  |      * @param leafletMap: a UIEventSource possible having a leaflet map | ||||||
|  |      * @constructor | ||||||
|  |      */ | ||||||
|  |     public AddClusteringToMap(leafletMap: UIEventSource<any>) { | ||||||
|  |         const clustering = this.layoutToUse.clustering | ||||||
|  |         new ShowDataLayer({ | ||||||
|  |             features: this.featureAggregator.getCountsForZoom(clustering, this.locationControl, clustering.minNeededElements), | ||||||
|  |             leafletMap: leafletMap, | ||||||
|  |             layerToShow: ShowTileInfo.styling, | ||||||
|  |             enablePopups: false, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public setupClickDialogOnMap(filterViewIsOpened: UIEventSource<boolean>, leafletMap: UIEventSource<any>) { | ||||||
|  | 
 | ||||||
|  |         const self = this | ||||||
|  |         function setup(){ | ||||||
|  |             let presetCount = 0; | ||||||
|  |             for (const layer of self.layoutToUse.layers) { | ||||||
|  |                 for (const preset of layer.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, filterViewIsOpened, self), | ||||||
|  |                 "new", | ||||||
|  |                 newPointDialogIsShown | ||||||
|  |             ); | ||||||
|  |             addNewPoint.isShown.addCallback((isShown) => { | ||||||
|  |                 if (!isShown) { | ||||||
|  |                     self.LastClickLocation.setData(undefined); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             new StrayClickHandler( | ||||||
|  |                 self.LastClickLocation, | ||||||
|  |                 self.selectedElement, | ||||||
|  |                 self.filteredLayers, | ||||||
|  |                 leafletMap, | ||||||
|  |                 addNewPoint | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.featureSwitchAddNew.addCallbackAndRunD(addNewAllowed => { | ||||||
|  |             if (addNewAllowed) { | ||||||
|  |                 setup() | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										208
									
								
								Logic/State/FeatureSwitchState.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								Logic/State/FeatureSwitchState.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,208 @@ | ||||||
|  | /** | ||||||
|  |  * The part of the global state which initializes the feature switches, based on default values and on the layoutToUse | ||||||
|  |  */ | ||||||
|  | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import {UIEventSource} from "../UIEventSource"; | ||||||
|  | import {QueryParameters} from "../Web/QueryParameters"; | ||||||
|  | import Constants from "../../Models/Constants"; | ||||||
|  | 
 | ||||||
|  | export default class FeatureSwitchState { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The layout that is being used in this run | ||||||
|  |      */ | ||||||
|  |     public readonly layoutToUse: LayoutConfig; | ||||||
|  | 
 | ||||||
|  |     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 featureSwitchIframePopoutEnabled: 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>; | ||||||
|  |     public readonly osmApiTileSize: UIEventSource<number>; | ||||||
|  |     public readonly backgroundLayerId: UIEventSource<string>; | ||||||
|  | 
 | ||||||
|  |     protected constructor(layoutToUse: LayoutConfig) { | ||||||
|  |         this.layoutToUse = layoutToUse; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         // Helper function to initialize feature switches
 | ||||||
|  |         function featSw( | ||||||
|  |             key: string, | ||||||
|  |             deflt: (layout: LayoutConfig) => boolean, | ||||||
|  |             documentation: string | ||||||
|  |         ): UIEventSource<boolean> { | ||||||
|  | 
 | ||||||
|  |             const defaultValue = deflt(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" | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         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.featureSwitchIframePopoutEnabled = featSw( | ||||||
|  |             "fs-iframe-popout", | ||||||
|  |             (layoutToUse) => layoutToUse?.enableIframePopout, | ||||||
|  |             "Disables/Enables the iframe-popout button. If in iframe mode and the welcome message is hidden, a popout button to the full mapcomplete instance is shown instead (unless disabled with this switch)" | ||||||
|  |         ); | ||||||
|  |         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.featureSwitchApiURL = QueryParameters.GetQueryParameter( | ||||||
|  |             "backend", | ||||||
|  |             "osm", | ||||||
|  |             "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'" | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         let testingDefaultValue = false; | ||||||
|  |         if (this.featureSwitchApiURL.data !== "osm-test" && | ||||||
|  |             (location.hostname === "localhost" || location.hostname === "127.0.0.1")) { | ||||||
|  |             testingDefaultValue = true | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         this.featureSwitchIsTesting = QueryParameters.GetQueryParameter( | ||||||
|  |             "test", | ||||||
|  |             ""+testingDefaultValue, | ||||||
|  |             "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.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 = UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassTimeout", | ||||||
|  |             "" + layoutToUse?.overpassTimeout, | ||||||
|  |             "Set a different timeout (in seconds) for queries in overpass")) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         this.overpassMaxZoom = | ||||||
|  |             UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassMaxZoom", | ||||||
|  |                 "" + layoutToUse?.overpassMaxZoom, | ||||||
|  |                 " point to switch between OSM-api and overpass")) | ||||||
|  | 
 | ||||||
|  |         this.osmApiTileSize = | ||||||
|  |             UIEventSource.asFloat(QueryParameters.GetQueryParameter("osmApiTileSize", | ||||||
|  |                 "" + layoutToUse?.osmApiTileSize, | ||||||
|  |                 "Tilesize when the OSM-API is used to fetch data within a BBOX")) | ||||||
|  | 
 | ||||||
|  |         this.featureSwitchUserbadge.addCallbackAndRun(userbadge => { | ||||||
|  |             if (!userbadge) { | ||||||
|  |                 this.featureSwitchAddNew.setData(false) | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         this.backgroundLayerId = QueryParameters.GetQueryParameter( | ||||||
|  |             "background", | ||||||
|  |             layoutToUse?.defaultBackgroundId ?? "osm", | ||||||
|  |             "The id of the background layer to start with" | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										272
									
								
								Logic/State/MapState.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								Logic/State/MapState.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,272 @@ | ||||||
|  | import UserRelatedState from "./UserRelatedState"; | ||||||
|  | import {UIEventSource} from "../UIEventSource"; | ||||||
|  | import BaseLayer from "../../Models/BaseLayer"; | ||||||
|  | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import AvailableBaseLayers from "../Actors/AvailableBaseLayers"; | ||||||
|  | import BackgroundLayerResetter from "../Actors/BackgroundLayerResetter"; | ||||||
|  | import Attribution from "../../UI/BigComponents/Attribution"; | ||||||
|  | import Minimap, {MinimapObj} from "../../UI/Base/Minimap"; | ||||||
|  | import {Tiles} from "../../Models/TileRange"; | ||||||
|  | import * as L from "leaflet"; | ||||||
|  | import Img from "../../UI/Base/Img"; | ||||||
|  | import Svg from "../../Svg"; | ||||||
|  | import BaseUIElement from "../../UI/BaseUIElement"; | ||||||
|  | import FilteredLayer from "../../Models/FilteredLayer"; | ||||||
|  | import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; | ||||||
|  | import {QueryParameters} from "../Web/QueryParameters"; | ||||||
|  | import * as personal from "../../assets/themes/personal/personal.json"; | ||||||
|  | import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; | ||||||
|  | import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Contains all the leaflet-map related state | ||||||
|  |  */ | ||||||
|  | export default class MapState extends UserRelatedState { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      The leaflet instance of the big basemap | ||||||
|  |      */ | ||||||
|  |     public leafletMap = new UIEventSource<L.Map>(undefined, "leafletmap"); | ||||||
|  |     /** | ||||||
|  |      * A list of currently available background layers | ||||||
|  |      */ | ||||||
|  |     public availableBackgroundLayers: UIEventSource<BaseLayer[]>; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The current background layer | ||||||
|  |      */ | ||||||
|  |     public backgroundLayer: UIEventSource<BaseLayer>; | ||||||
|  |     /** | ||||||
|  |      * 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 readonly mainMapObject: BaseUIElement & MinimapObj; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * WHich layers are enabled in the current theme | ||||||
|  |      */ | ||||||
|  |     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers"); | ||||||
|  |     /** | ||||||
|  |      * Which overlays are shown | ||||||
|  |      */ | ||||||
|  |     public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[] | ||||||
|  | 
 | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  |     constructor(layoutToUse: LayoutConfig) { | ||||||
|  |         super(layoutToUse); | ||||||
|  | 
 | ||||||
|  |         this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl); | ||||||
|  | 
 | ||||||
|  |         this.backgroundLayer = this.backgroundLayerId.map( | ||||||
|  |             (selectedId: string) => { | ||||||
|  |                 if (selectedId === undefined) { | ||||||
|  |                     return AvailableBaseLayers.osmCarto; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 const available = this.availableBackgroundLayers.data; | ||||||
|  |                 for (const layer of available) { | ||||||
|  |                     if (layer.id === selectedId) { | ||||||
|  |                         return layer; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 return AvailableBaseLayers.osmCarto; | ||||||
|  |             }, | ||||||
|  |             [this.availableBackgroundLayers], | ||||||
|  |             (layer) => layer.id | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |         * Selects a different background layer if the background layer has no coverage at the current location | ||||||
|  |          */ | ||||||
|  |         new BackgroundLayerResetter( | ||||||
|  |             this.backgroundLayer, | ||||||
|  |             this.locationControl, | ||||||
|  |             this.availableBackgroundLayers, | ||||||
|  |             this.layoutToUse.defaultBackgroundId | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         const attr = new Attribution( | ||||||
|  |             this.locationControl, | ||||||
|  |             this.osmConnection.userDetails, | ||||||
|  |             this.layoutToUse, | ||||||
|  |             this.currentBounds | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         // Will write into this.leafletMap
 | ||||||
|  |         this.mainMapObject = Minimap.createMiniMap({ | ||||||
|  |             background: this.backgroundLayer, | ||||||
|  |             location: this.locationControl, | ||||||
|  |             leafletMap: this.leafletMap, | ||||||
|  |             bounds: this.currentBounds, | ||||||
|  |             attribution: attr, | ||||||
|  |             lastClickLocation: this.LastClickLocation | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         | ||||||
|  |         this.overlayToggles = this.layoutToUse.tileLayerSources.filter(c => c.name !== undefined).map(c => ({ | ||||||
|  |             config: c, | ||||||
|  |             isDisplayed: QueryParameters.GetQueryParameter("overlay-" + c.id, "" + c.defaultState, "Wether or not the overlay " + c.id + " is shown").map(str => str === "true", [], b => "" + b) | ||||||
|  |         })) | ||||||
|  |         this.filteredLayers = this.InitializeFilteredLayers() | ||||||
|  |          | ||||||
|  |          | ||||||
|  |         this.lockBounds() | ||||||
|  |         this.AddAllOverlaysToMap(this.leafletMap) | ||||||
|  |         this.addHomeMarker() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private addHomeMarker() { | ||||||
|  |         const leafletMap = this.leafletMap | ||||||
|  |         const osmConnection = this.osmConnection | ||||||
|  | 
 | ||||||
|  |         function addHomeMarker() { | ||||||
|  |             const userDetails = osmConnection.userDetails.data; | ||||||
|  |             if (userDetails === undefined) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             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 = 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], | ||||||
|  |             }); | ||||||
|  |             const marker = L.marker([home.lat, home.lon], {icon: icon}); | ||||||
|  |             marker.addTo(leaflet); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         osmConnection.userDetails.addCallbackAndRunD(_ => addHomeMarker()); | ||||||
|  |         leafletMap.addCallbackAndRunD(_ => addHomeMarker()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private lockBounds() { | ||||||
|  |         const layout = this.layoutToUse; | ||||||
|  |         if (layout.lockLocation) { | ||||||
|  |             if (layout.lockLocation === true) { | ||||||
|  |                 const tile = Tiles.embedded_tile( | ||||||
|  |                     layout.startLat, | ||||||
|  |                     layout.startLon, | ||||||
|  |                     layout.startZoom - 1 | ||||||
|  |                 ); | ||||||
|  |                 const bounds = Tiles.tile_bounds(tile.z, tile.x, tile.y); | ||||||
|  |                 // 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); | ||||||
|  |             this.leafletMap.addCallbackAndRunD(map => { | ||||||
|  |                 // @ts-ignore
 | ||||||
|  |                 map.setMaxBounds(layout.lockLocation); | ||||||
|  |                 map.setMinZoom(layout.startZoom); | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     private InitializeFilteredLayers() { | ||||||
|  |         // Initialize the filtered layers state
 | ||||||
|  | 
 | ||||||
|  |         const layoutToUse = this.layoutToUse; | ||||||
|  |         const empty = [] | ||||||
|  |         const flayers: FilteredLayer[] = []; | ||||||
|  |         for (const layer of layoutToUse.layers) { | ||||||
|  |             let isDisplayed: UIEventSource<boolean> | ||||||
|  |             if (layoutToUse.id === personal.id) { | ||||||
|  |                 isDisplayed = this.osmConnection.GetPreference("personal-theme-layer-" + layer.id + "-enabled") | ||||||
|  |                     .map(value => value === "yes", [], enabled => { | ||||||
|  |                         return enabled ? "yes" : ""; | ||||||
|  |                     }) | ||||||
|  |                 isDisplayed.addCallbackAndRun(d => console.log("IsDisplayed for layer", layer.id, "is currently", d)) | ||||||
|  |             } else { | ||||||
|  |                 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, | ||||||
|  |                 appliedFilters: new UIEventSource<{ filter: FilterConfig, selected: number }[]>([]), | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             if (layer.filters.length > 0) { | ||||||
|  |                 const filtersPerName = new Map<string, FilterConfig>() | ||||||
|  |                 layer.filters.forEach(f => filtersPerName.set(f.id, f)) | ||||||
|  |                 const qp = QueryParameters.GetQueryParameter("filter-" + layer.id, "", "Filtering state for a layer") | ||||||
|  |                 flayer.appliedFilters.map(filters => { | ||||||
|  |                     filters = filters ?? [] | ||||||
|  |                     return filters.map(f => f.filter.id + "." + f.selected).join(",") | ||||||
|  |                 }, [], textual => { | ||||||
|  |                     if (textual.length === 0) { | ||||||
|  |                         return empty | ||||||
|  |                     } | ||||||
|  |                     return textual.split(",").map(part => { | ||||||
|  |                         const [filterId, selected] = part.split("."); | ||||||
|  |                         return {filter: filtersPerName.get(filterId), selected: Number(selected)} | ||||||
|  |                     }).filter(f => f.filter !== undefined && !isNaN(f.selected)) | ||||||
|  |                 }).syncWith(qp, true) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             flayers.push(flayer); | ||||||
|  |         } | ||||||
|  |         return new UIEventSource<FilteredLayer[]>(flayers); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public AddAllOverlaysToMap(leafletMap: UIEventSource<any>){ | ||||||
|  |         const initialized =new Set() | ||||||
|  |         for (const overlayToggle of this.overlayToggles) { | ||||||
|  |             new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed) | ||||||
|  |             initialized.add(overlayToggle.config) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (const tileLayerSource of this.layoutToUse.tileLayerSources) { | ||||||
|  |             if (initialized.has(tileLayerSource)) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             new ShowOverlayLayer(tileLayerSource, leafletMap) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										107
									
								
								Logic/State/UserRelatedState.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								Logic/State/UserRelatedState.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,107 @@ | ||||||
|  | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import {OsmConnection} from "../Osm/OsmConnection"; | ||||||
|  | import {MangroveIdentity} from "../Web/MangroveReviews"; | ||||||
|  | import {UIEventSource} from "../UIEventSource"; | ||||||
|  | import {QueryParameters} from "../Web/QueryParameters"; | ||||||
|  | import InstalledThemes from "../Actors/InstalledThemes"; | ||||||
|  | import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||||
|  | import {Utils} from "../../Utils"; | ||||||
|  | import Locale from "../../UI/i18n/Locale"; | ||||||
|  | import ElementsState from "./ElementsState"; | ||||||
|  | import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, | ||||||
|  |  * which layers they enabled, ... | ||||||
|  |  */ | ||||||
|  | export default class UserRelatedState extends ElementsState { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      The user credentials | ||||||
|  |      */ | ||||||
|  |     public osmConnection: OsmConnection; | ||||||
|  |     /** | ||||||
|  |      * The key for mangrove | ||||||
|  |      */ | ||||||
|  |     public mangroveIdentity: MangroveIdentity; | ||||||
|  |     /** | ||||||
|  |      * Which layers are enabled in the personal theme | ||||||
|  |      */ | ||||||
|  |     public favouriteLayers: UIEventSource<string[]>; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * WHich other themes the user previously visited | ||||||
|  |      */ | ||||||
|  |     public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; | ||||||
|  | 
 | ||||||
|  |     constructor(layoutToUse: LayoutConfig) { | ||||||
|  |         super(layoutToUse); | ||||||
|  | 
 | ||||||
|  |         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 | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         this.mangroveIdentity = new MangroveIdentity( | ||||||
|  |             this.osmConnection.GetLongPreference("identity", "mangrove") | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         if (layoutToUse?.hideFromOverview) { | ||||||
|  |             this.osmConnection | ||||||
|  |                 .GetPreference("hidden-theme-" + layoutToUse?.id + "-enabled") | ||||||
|  |                 .setData("true"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         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(";") | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         this.InitializeLanguage(); | ||||||
|  |         new SelectedElementTagsUpdater(this) | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private InitializeLanguage() { | ||||||
|  |         const layoutToUse = this.layoutToUse; | ||||||
|  |         Locale.language.syncWith(this.osmConnection.GetPreference("language")); | ||||||
|  |         Locale.language | ||||||
|  |             .addCallback((currentLanguage) => { | ||||||
|  |                 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(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -318,6 +318,22 @@ export class UIEventSource<T> { | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public 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); | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class UIEventSourceTools { | export class UIEventSourceTools { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | ||||||
| import {LayerConfigJson} from "./LayerConfigJson"; | import {LayerConfigJson} from "./LayerConfigJson"; | ||||||
| import TilesourceConfig from "../TilesourceConfig"; |  | ||||||
| import TilesourceConfigJson from "./TilesourceConfigJson"; | import TilesourceConfigJson from "./TilesourceConfigJson"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -16,7 +15,7 @@ import TilesourceConfigJson from "./TilesourceConfigJson"; | ||||||
|  * General remark: a type (string | any) indicates either a fixed or a translatable string. |  * General remark: a type (string | any) indicates either a fixed or a translatable string. | ||||||
|  */ |  */ | ||||||
| export interface LayoutConfigJson { | export interface LayoutConfigJson { | ||||||
| 
 |     | ||||||
|     /** |     /** | ||||||
|      * The id of this layout. |      * The id of this layout. | ||||||
|      * |      * | ||||||
|  | @ -106,7 +105,20 @@ export interface LayoutConfigJson { | ||||||
|      * IF widenfactor is 1, this feature is disabled. A recommended value is between 1 and 3 |      * IF widenfactor is 1, this feature is disabled. A recommended value is between 1 and 3 | ||||||
|      */ |      */ | ||||||
|     widenFactor?: number; |     widenFactor?: number; | ||||||
|  |     /** | ||||||
|  |      * At low zoom levels, overpass is used to query features. | ||||||
|  |      * At high zoom level, the OSM api is used to fetch one or more BBOX aligning with a slippy tile. | ||||||
|  |      * The overpassMaxZoom controls the flipoverpoint: if the zoom is this or lower, overpass is used. | ||||||
|  |      */ | ||||||
|  |     overpassMaxZoom?: 17 | number | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * When the OSM-api is used to fetch features, it does so in a tiled fashion. | ||||||
|  |      * These tiles are using a ceratin zoom level, that can be controlled here | ||||||
|  |      * Default: overpassMaxZoom + 1 | ||||||
|  |      */ | ||||||
|  |     osmApiTileSize: number | ||||||
|  |      | ||||||
|     /** |     /** | ||||||
|      * A tagrendering depicts how to show some tags or how to show a question for it. |      * A tagrendering depicts how to show some tags or how to show a question for it. | ||||||
|      * |      * | ||||||
|  | @ -269,6 +281,7 @@ export interface LayoutConfigJson { | ||||||
|     enableShowAllQuestions?: boolean; |     enableShowAllQuestions?: boolean; | ||||||
|     enableDownload?: boolean; |     enableDownload?: boolean; | ||||||
|     enablePdfDownload?: boolean; |     enablePdfDownload?: boolean; | ||||||
|  |     enableIframePopout?: true | boolean; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Set one or more overpass URLs to use for this theme.. |      * Set one or more overpass URLs to use for this theme.. | ||||||
|  |  | ||||||
|  | @ -46,6 +46,7 @@ export default class LayoutConfig { | ||||||
|     public readonly enableShowAllQuestions: boolean; |     public readonly enableShowAllQuestions: boolean; | ||||||
|     public readonly enableExportButton: boolean; |     public readonly enableExportButton: boolean; | ||||||
|     public readonly enablePdfDownload: boolean; |     public readonly enablePdfDownload: boolean; | ||||||
|  |     public readonly enableIframePopout: boolean; | ||||||
| 
 | 
 | ||||||
|     public readonly customCss?: string; |     public readonly customCss?: string; | ||||||
|     /* |     /* | ||||||
|  | @ -54,8 +55,10 @@ export default class LayoutConfig { | ||||||
|     public readonly cacheTimeout?: number; |     public readonly cacheTimeout?: number; | ||||||
|     public readonly overpassUrl: string[]; |     public readonly overpassUrl: string[]; | ||||||
|     public readonly overpassTimeout: number; |     public readonly overpassTimeout: number; | ||||||
|  |     public readonly overpassMaxZoom: number | ||||||
|  |     public readonly osmApiTileSize: number | ||||||
|     public readonly official: boolean; |     public readonly official: boolean; | ||||||
| 
 |   | ||||||
|     constructor(json: LayoutConfigJson, official = true, context?: string) { |     constructor(json: LayoutConfigJson, official = true, context?: string) { | ||||||
|         this.official = official; |         this.official = official; | ||||||
|         this.id = json.id; |         this.id = json.id; | ||||||
|  | @ -171,6 +174,7 @@ export default class LayoutConfig { | ||||||
|         this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; |         this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; | ||||||
|         this.enableExportButton = json.enableDownload ?? false; |         this.enableExportButton = json.enableDownload ?? false; | ||||||
|         this.enablePdfDownload = json.enablePdfDownload ?? false; |         this.enablePdfDownload = json.enablePdfDownload ?? false; | ||||||
|  |         this.enableIframePopout = json.enableIframePopout ?? true | ||||||
|         this.customCss = json.customCss; |         this.customCss = json.customCss; | ||||||
|         this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) |         this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) | ||||||
|         this.overpassUrl = Constants.defaultOverpassUrls |         this.overpassUrl = Constants.defaultOverpassUrls | ||||||
|  | @ -182,6 +186,8 @@ export default class LayoutConfig { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         this.overpassTimeout = json.overpassTimeout ?? 30 |         this.overpassTimeout = json.overpassTimeout ?? 30 | ||||||
|  |         this.overpassMaxZoom = json.overpassMaxZoom ?? 17 | ||||||
|  |         this.osmApiTileSize = json.osmApiTileSize ?? this.overpassMaxZoom + 1 | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										441
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										441
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -1,448 +1,19 @@ | ||||||
| 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 LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; | ||||||
| import {BBox} from "./Logic/BBox"; | import FeaturePipelineState from "./Logic/State/FeaturePipelineState"; | ||||||
| import SelectedElementTagsUpdater from "./Logic/Actors/SelectedElementTagsUpdater"; |  | ||||||
| import TilesourceConfig from "./Models/ThemeConfig/TilesourceConfig"; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Contains the global state: a bunch of UI-event sources |  * Contains the global state: a bunch of UI-event sources | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| export default class State { | export default class State extends FeaturePipelineState { | ||||||
|     // The singleton of the global 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(); |     public static state: FeaturePipelineState; | ||||||
|     /** |  | ||||||
|      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"); |  | ||||||
| 
 |  | ||||||
|     public  overlayToggles : { config: TilesourceConfig, isDisplayed: UIEventSource<boolean>}[] |  | ||||||
|      |  | ||||||
|     /** |  | ||||||
|      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) { |     constructor(layoutToUse: LayoutConfig) { | ||||||
|         const self = this; |         super(layoutToUse) | ||||||
|         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); |  | ||||||
|          |  | ||||||
|         this.overlayToggles = this.layoutToUse.tileLayerSources.filter(c => c.name !== undefined).map(c => ({ |  | ||||||
|             config: c, |  | ||||||
|             isDisplayed: QueryParameters.GetQueryParameter("overlay-"+c.id, ""+c.defaultState,"Wether or not the overlay "+c.id+" is shown").map(str => str === "true", [], b => ""+b) |  | ||||||
|         })) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     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); |  | ||||||
|             } |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								UI/AllThemesGui.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								UI/AllThemesGui.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | import {FixedUiElement} from "./Base/FixedUiElement"; | ||||||
|  | import State from "../State"; | ||||||
|  | import Combine from "./Base/Combine"; | ||||||
|  | import MoreScreen from "./BigComponents/MoreScreen"; | ||||||
|  | import Translations from "./i18n/Translations"; | ||||||
|  | import Constants from "../Models/Constants"; | ||||||
|  | import UserRelatedState from "../Logic/State/UserRelatedState"; | ||||||
|  | 
 | ||||||
|  | export default class AllThemesGui { | ||||||
|  |     constructor() { | ||||||
|  |         new FixedUiElement("").AttachTo("centermessage") | ||||||
|  |        const state = new UserRelatedState(undefined); | ||||||
|  |         new Combine([new MoreScreen(state, true), | ||||||
|  |             Translations.t.general.aboutMapcomplete.SetClass("link-underline"), | ||||||
|  |             new FixedUiElement("v" + Constants.vNumber) | ||||||
|  |         ]).SetClass("block m-5 lg:w-3/4 lg:ml-40") | ||||||
|  |             .SetStyle("pointer-events: all;") | ||||||
|  |             .AttachTo("topleft-tools"); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| import State from "../../State"; |  | ||||||
| import ThemeIntroductionPanel from "./ThemeIntroductionPanel"; | import ThemeIntroductionPanel from "./ThemeIntroductionPanel"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
|  | @ -8,31 +7,46 @@ import Constants from "../../Models/Constants"; | ||||||
| import Combine from "../Base/Combine"; | import Combine from "../Base/Combine"; | ||||||
| import {TabbedComponent} from "../Base/TabbedComponent"; | import {TabbedComponent} from "../Base/TabbedComponent"; | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import UserDetails from "../../Logic/Osm/OsmConnection"; | import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import Toggle from "../Input/Toggle"; | import Toggle from "../Input/Toggle"; | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
|  | import UserRelatedState from "../../Logic/State/UserRelatedState"; | ||||||
| 
 | 
 | ||||||
| export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     constructor(isShown: UIEventSource<boolean>) { |     constructor(isShown: UIEventSource<boolean>, | ||||||
|         const layoutToUse = State.state.layoutToUse; |                 currentTab: UIEventSource<number>, | ||||||
|  |                 state: { | ||||||
|  |                     layoutToUse: LayoutConfig, | ||||||
|  |                     osmConnection: OsmConnection, | ||||||
|  |                     featureSwitchShareScreen: UIEventSource<boolean>, | ||||||
|  |                     featureSwitchMoreQuests: UIEventSource<boolean> | ||||||
|  |                 } & UserRelatedState) { | ||||||
|  |         const layoutToUse = state.layoutToUse; | ||||||
|         super( |         super( | ||||||
|             () => layoutToUse.title.Clone(), |             () => layoutToUse.title.Clone(), | ||||||
|             () => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails, isShown), |             () => FullWelcomePaneWithTabs.GenerateContents(state, currentTab, isShown), | ||||||
|             "welcome", isShown |             undefined, isShown | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static ConstructBaseTabs(layoutToUse: LayoutConfig, isShown: UIEventSource<boolean>): { header: string | BaseUIElement; content: BaseUIElement }[] { |     private static ConstructBaseTabs(state: { | ||||||
|  |                                          layoutToUse: LayoutConfig, | ||||||
|  |                                          osmConnection: OsmConnection, | ||||||
|  |                                          featureSwitchShareScreen: UIEventSource<boolean>, | ||||||
|  |                                          featureSwitchMoreQuests: UIEventSource<boolean> | ||||||
|  |                                      } & UserRelatedState, | ||||||
|  |                                      isShown: UIEventSource<boolean>): | ||||||
|  |         { header: string | BaseUIElement; content: BaseUIElement }[] { | ||||||
| 
 | 
 | ||||||
|         let welcome: BaseUIElement = new ThemeIntroductionPanel(isShown); |         let welcome: BaseUIElement = new ThemeIntroductionPanel(isShown); | ||||||
|         | 
 | ||||||
|         const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [ |         const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [ | ||||||
|             {header: `<img src='${layoutToUse.icon}'>`, content: welcome}, |             {header: `<img src='${state.layoutToUse.icon}'>`, content: welcome}, | ||||||
|             { |             { | ||||||
|                 header: Svg.osm_logo_img, |                 header: Svg.osm_logo_img, | ||||||
|                 content: Translations.t.general.openStreetMapIntro.Clone().SetClass("link-underline") |                 content: Translations.t.general.openStreetMapIntro.Clone().SetClass("link-underline") | ||||||
|  | @ -40,31 +54,36 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | ||||||
| 
 | 
 | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|         if (State.state.featureSwitchShareScreen.data) { |         if (state.featureSwitchShareScreen.data) { | ||||||
|             tabs.push({header: Svg.share_img, content: new ShareScreen()}); |             tabs.push({header: Svg.share_img, content: new ShareScreen()}); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (State.state.featureSwitchMoreQuests.data) { |         if (state.featureSwitchMoreQuests.data) { | ||||||
| 
 | 
 | ||||||
|             tabs.push({ |             tabs.push({ | ||||||
|                 header: Svg.add_img, |                 header: Svg.add_img, | ||||||
|                 content: new MoreScreen() |                 content: new MoreScreen(state) | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return tabs; |         return tabs; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static GenerateContents(layoutToUse: LayoutConfig, userDetails: UIEventSource<UserDetails>, isShown: UIEventSource<boolean>) { |     private static GenerateContents(state: { | ||||||
|  |         layoutToUse: LayoutConfig, | ||||||
|  |         osmConnection: OsmConnection, | ||||||
|  |         featureSwitchShareScreen: UIEventSource<boolean>, | ||||||
|  |         featureSwitchMoreQuests: UIEventSource<boolean> | ||||||
|  |     } & UserRelatedState, currentTab: UIEventSource<number>, isShown: UIEventSource<boolean>) { | ||||||
| 
 | 
 | ||||||
|         const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown) |         const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown) | ||||||
|         const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)] |         const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown)] | ||||||
| 
 | 
 | ||||||
|         const now = new Date() |         const now = new Date() | ||||||
|         const lastWeek = new Date(now.getDate() - 7 * 24 * 60 * 60 * 1000) |         const lastWeek = new Date(now.getDate() - 7 * 24 * 60 * 60 * 1000) | ||||||
|         const date = lastWeek.getFullYear()+"-"+Utils.TwoDigits(lastWeek.getMonth()+1)+"-"+Utils.TwoDigits(lastWeek.getDate()) |         const date = lastWeek.getFullYear() + "-" + Utils.TwoDigits(lastWeek.getMonth() + 1) + "-" + Utils.TwoDigits(lastWeek.getDate()) | ||||||
|         const osmcha_link = `https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%22${date}%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D` |         const osmcha_link = `https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%22${date}%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D` | ||||||
|          | 
 | ||||||
|         tabsWithAboutMc.push({ |         tabsWithAboutMc.push({ | ||||||
|                 header: Svg.help, |                 header: Svg.help, | ||||||
|                 content: new Combine([Translations.t.general.aboutMapcomplete.Clone() |                 content: new Combine([Translations.t.general.aboutMapcomplete.Clone() | ||||||
|  | @ -75,11 +94,11 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | ||||||
| 
 | 
 | ||||||
|         tabs.forEach(c => c.content.SetClass("p-4")) |         tabs.forEach(c => c.content.SetClass("p-4")) | ||||||
|         tabsWithAboutMc.forEach(c => c.content.SetClass("p-4")) |         tabsWithAboutMc.forEach(c => c.content.SetClass("p-4")) | ||||||
|          | 
 | ||||||
|         return new Toggle( |         return new Toggle( | ||||||
|             new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab), |             new TabbedComponent(tabsWithAboutMc, currentTab), | ||||||
|             new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab), |             new TabbedComponent(tabs, currentTab), | ||||||
|             userDetails.map((userdetails: UserDetails) => |             state.osmConnection.userDetails.map((userdetails: UserDetails) => | ||||||
|                 userdetails.loggedIn && |                 userdetails.loggedIn && | ||||||
|                 userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock) |                 userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ import Combine from "../Base/Combine"; | ||||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
| import AttributionPanel from "./AttributionPanel"; | import AttributionPanel from "./AttributionPanel"; | ||||||
| import State from "../../State"; |  | ||||||
| import ContributorCount from "../../Logic/ContributorCount"; | import ContributorCount from "../../Logic/ContributorCount"; | ||||||
| import Toggle from "../Input/Toggle"; | import Toggle from "../Input/Toggle"; | ||||||
| import MapControlButton from "../MapControlButton"; | import MapControlButton from "../MapControlButton"; | ||||||
|  | @ -13,16 +12,33 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; | import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc"; | ||||||
| import {BBox} from "../../Logic/BBox"; | import {BBox} from "../../Logic/BBox"; | ||||||
|  | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import FilteredLayer from "../../Models/FilteredLayer"; | ||||||
| 
 | 
 | ||||||
| export default class LeftControls extends Combine { | export default class LeftControls extends Combine { | ||||||
| 
 | 
 | ||||||
|     constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc>, overlayToggles: any}) { |     constructor(state: { | ||||||
|  |                     layoutToUse: LayoutConfig, | ||||||
|  |                     featurePipeline: FeaturePipeline, | ||||||
|  |                     currentBounds: UIEventSource<BBox>, | ||||||
|  |                     locationControl: UIEventSource<Loc>, | ||||||
|  |                     overlayToggles: any, | ||||||
|  |                     featureSwitchEnableExport: UIEventSource<boolean>, | ||||||
|  |                     featureSwitchExportAsPdf: UIEventSource<boolean>, | ||||||
|  |                     filteredLayers: UIEventSource<FilteredLayer[]>, | ||||||
|  |                     featureSwitchFilter: UIEventSource<boolean>, | ||||||
|  |                     selectedElement: UIEventSource<any> | ||||||
|  |                 }, | ||||||
|  |                 guiState: { | ||||||
|  |                     downloadControlIsOpened: UIEventSource<boolean>, | ||||||
|  |                     filterViewIsOpened: UIEventSource<boolean>, | ||||||
|  |                 }) { | ||||||
| 
 | 
 | ||||||
|         const toggledCopyright = new ScrollableFullScreen( |         const toggledCopyright = new ScrollableFullScreen( | ||||||
|             () => Translations.t.general.attribution.attributionTitle.Clone(), |             () => Translations.t.general.attribution.attributionTitle.Clone(), | ||||||
|             () => |             () => | ||||||
|                 new AttributionPanel( |                 new AttributionPanel( | ||||||
|                     State.state.layoutToUse, |                     state.layoutToUse, | ||||||
|                     new ContributorCount(state).Contributors |                     new ContributorCount(state).Contributors | ||||||
|                 ), |                 ), | ||||||
|             undefined |             undefined | ||||||
|  | @ -38,50 +54,50 @@ export default class LeftControls extends Combine { | ||||||
| 
 | 
 | ||||||
|         const toggledDownload = new Toggle( |         const toggledDownload = new Toggle( | ||||||
|             new AllDownloads( |             new AllDownloads( | ||||||
|                 State.state.downloadControlIsOpened |                 guiState.downloadControlIsOpened | ||||||
|             ).SetClass("block p-1 rounded-full"), |             ).SetClass("block p-1 rounded-full"), | ||||||
|             new MapControlButton(Svg.download_svg()) |             new MapControlButton(Svg.download_svg()) | ||||||
|                 .onClick(() => State.state.downloadControlIsOpened.setData(true)), |                 .onClick(() => guiState.downloadControlIsOpened.setData(true)), | ||||||
|             State.state.downloadControlIsOpened |             guiState.downloadControlIsOpened | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         const downloadButtonn = new Toggle( |         const downloadButtonn = new Toggle( | ||||||
|             toggledDownload, |             toggledDownload, | ||||||
|             undefined, |             undefined, | ||||||
|             State.state.featureSwitchEnableExport.map(downloadEnabled => downloadEnabled || State.state.featureSwitchExportAsPdf.data, |             state.featureSwitchEnableExport.map(downloadEnabled => downloadEnabled || state.featureSwitchExportAsPdf.data, | ||||||
|                 [State.state.featureSwitchExportAsPdf]) |                 [state.featureSwitchExportAsPdf]) | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         const toggledFilter = new Toggle( |         const toggledFilter = new Toggle( | ||||||
|             new ScrollableFullScreen( |             new ScrollableFullScreen( | ||||||
|                 () => Translations.t.general.layerSelection.title.Clone(), |                 () => Translations.t.general.layerSelection.title.Clone(), | ||||||
|                 () => |                 () => | ||||||
|                     new FilterView(State.state.filteredLayers, state.overlayToggles).SetClass( |                     new FilterView(state.filteredLayers, state.overlayToggles).SetClass( | ||||||
|                         "block p-1 rounded-full" |                         "block p-1 rounded-full" | ||||||
|                     ), |                     ), | ||||||
|                 undefined, |                 undefined, | ||||||
|                 State.state.filterIsOpened |                 guiState.filterViewIsOpened | ||||||
|             ), |             ), | ||||||
|             new MapControlButton(Svg.filter_svg()) |             new MapControlButton(Svg.filter_svg()) | ||||||
|                 .onClick(() => State.state.filterIsOpened.setData(true)), |                 .onClick(() => guiState.filterViewIsOpened.setData(true)), | ||||||
|             State.state.filterIsOpened |             guiState.filterViewIsOpened | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         const filterButton = new Toggle( |         const filterButton = new Toggle( | ||||||
|             toggledFilter, |             toggledFilter, | ||||||
|             undefined, |             undefined, | ||||||
|             State.state.featureSwitchFilter |             state.featureSwitchFilter | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         State.state.locationControl.addCallback(() => { |         state.locationControl.addCallback(() => { | ||||||
|             // Close the layer selection when the map is moved
 |             // Close the layer selection when the map is moved
 | ||||||
|             toggledDownload.isEnabled.setData(false); |             toggledDownload.isEnabled.setData(false); | ||||||
|             copyrightButton.isEnabled.setData(false); |             copyrightButton.isEnabled.setData(false); | ||||||
|             toggledFilter.isEnabled.setData(false); |             toggledFilter.isEnabled.setData(false); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         State.state.selectedElement.addCallbackAndRunD((_) => { |         state.selectedElement.addCallbackAndRunD((_) => { | ||||||
|             toggledDownload.isEnabled.setData(false); |             toggledDownload.isEnabled.setData(false); | ||||||
|             copyrightButton.isEnabled.setData(false); |             copyrightButton.isEnabled.setData(false); | ||||||
|             toggledFilter.isEnabled.setData(false); |             toggledFilter.isEnabled.setData(false); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
| import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; | import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import State from "../../State"; |  | ||||||
| import Combine from "../Base/Combine"; | import Combine from "../Base/Combine"; | ||||||
| import {SubtleButton} from "../Base/SubtleButton"; | import {SubtleButton} from "../Base/SubtleButton"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
|  | @ -11,15 +10,21 @@ import LanguagePicker from "../LanguagePicker"; | ||||||
| import IndexText from "./IndexText"; | import IndexText from "./IndexText"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
|  | import Loc from "../../Models/Loc"; | ||||||
|  | import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||||
|  | import UserRelatedState from "../../Logic/State/UserRelatedState"; | ||||||
|  | import Toggle from "../Input/Toggle"; | ||||||
|  | import {Utils} from "../../Utils"; | ||||||
|  | import Title from "../Base/Title"; | ||||||
| 
 | 
 | ||||||
| export default class MoreScreen extends Combine { | export default class MoreScreen extends Combine { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     constructor(onMainScreen: boolean = false) { |     constructor(state: UserRelatedState & { | ||||||
|         super(MoreScreen.Init(onMainScreen, State.state)); |         locationControl?: UIEventSource<Loc>, | ||||||
|     } |         layoutToUse?: LayoutConfig | ||||||
| 
 |     }, onMainScreen: boolean = false) { | ||||||
|     private static Init(onMainScreen: boolean, state: State): BaseUIElement [] { |  | ||||||
|         const tr = Translations.t.general.morescreen; |         const tr = Translations.t.general.morescreen; | ||||||
|         let intro: BaseUIElement = tr.intro.Clone(); |         let intro: BaseUIElement = tr.intro.Clone(); | ||||||
|         let themeButtonStyle = "" |         let themeButtonStyle = "" | ||||||
|  | @ -35,30 +40,59 @@ export default class MoreScreen extends Combine { | ||||||
|             themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4" |             themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4" | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return [ |         super([ | ||||||
|             intro, |             intro, | ||||||
|             MoreScreen.createOfficialThemesList(state, themeButtonStyle).SetClass(themeListStyle), |             MoreScreen.createOfficialThemesList(state, themeButtonStyle).SetClass(themeListStyle), | ||||||
|             MoreScreen.createUnofficialThemeList(themeButtonStyle)?.SetClass(themeListStyle), |             MoreScreen.createPreviouslyVistedHiddenList(state, themeButtonStyle).SetClass(themeListStyle), | ||||||
|  |             MoreScreen.createUnofficialThemeList(themeButtonStyle, state)?.SetClass(themeListStyle), | ||||||
|             tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10") |             tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10") | ||||||
|         ]; |         ]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static createUnofficialThemeList(buttonClass: string): BaseUIElement { | 
 | ||||||
|         return new VariableUiElement(State.state.installedThemes.map(customThemes => { |     private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState): BaseUIElement { | ||||||
|  |         return new VariableUiElement(state.installedThemes.map(customThemes => { | ||||||
|             const els: BaseUIElement[] = [] |             const els: BaseUIElement[] = [] | ||||||
|             if (customThemes.length > 0) { |             if (customThemes.length > 0) { | ||||||
|                 els.push(Translations.t.general.customThemeIntro.Clone()) |                 els.push(Translations.t.general.customThemeIntro.Clone()) | ||||||
| 
 | 
 | ||||||
|                 const customThemesElement = new Combine( |                 const customThemesElement = new Combine( | ||||||
|                     customThemes.map(theme => MoreScreen.createLinkButton(theme.layout, theme.definition)?.SetClass(buttonClass)) |                     customThemes.map(theme => MoreScreen.createLinkButton(state, theme.layout, theme.definition)?.SetClass(buttonClass)) | ||||||
|                 ) |                 ) | ||||||
|                 els.push(customThemesElement) |                 els.push(customThemesElement) | ||||||
|             } |             } | ||||||
|             return els; |             return els; | ||||||
|         })); |         })); | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string){ | ||||||
|  |         const  t= Translations.t.general.morescreen | ||||||
|  |         return new Toggle( | ||||||
|  |                 new Combine([ | ||||||
|  |                    new Title(t.previouslyHiddenTitle.Clone()), | ||||||
|  |             t.hiddenExplanation, | ||||||
|  |                      | ||||||
|  |                      | ||||||
|  |             new VariableUiElement( | ||||||
|  |                 state.osmConnection.preferencesHandler.preferences.map(allPreferences => { | ||||||
|  |                     const knownThemes = Utils.NoNull( Object.keys(allPreferences).filter(key => key.startsWith("hidden-theme-")) | ||||||
|  |                         .map(key => key.substr("hidden-theme-".length, key.length - "-enabled".length)) | ||||||
|  |                         .map(theme => AllKnownLayouts.allKnownLayouts.get(theme) )) | ||||||
|  |                     return new Combine(knownThemes.map(layout =>  | ||||||
|  |                         MoreScreen.createLinkButton(state, layout ).SetClass(buttonClass) | ||||||
|  |                     )) | ||||||
|  |                      | ||||||
|  |                 }) | ||||||
|  |                  | ||||||
|  |             )]).SetClass("flex flex-col"), | ||||||
|  |             undefined, | ||||||
|  |             state.osmConnection.isLoggedIn | ||||||
|  |         ) | ||||||
|  |          | ||||||
| 
 | 
 | ||||||
|     private static createOfficialThemesList(state: State, buttonClass: string): BaseUIElement { |     } | ||||||
|  | 
 | ||||||
|  |     private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource<Loc> }, buttonClass: string): BaseUIElement { | ||||||
|         let officialThemes = AllKnownLayouts.layoutsList |         let officialThemes = AllKnownLayouts.layoutsList | ||||||
| 
 | 
 | ||||||
|         let buttons = officialThemes.map((layout) => { |         let buttons = officialThemes.map((layout) => { | ||||||
|  | @ -66,10 +100,10 @@ export default class MoreScreen extends Combine { | ||||||
|                 console.trace("Layout is undefined") |                 console.trace("Layout is undefined") | ||||||
|                 return undefined |                 return undefined | ||||||
|             } |             } | ||||||
|             const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass); |             const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass); | ||||||
|             if (layout.id === personal.id) { |             if (layout.id === personal.id) { | ||||||
|                 return new VariableUiElement( |                 return new VariableUiElement( | ||||||
|                     State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount) |                     state.osmConnection.userDetails.map(userdetails => userdetails.csCount) | ||||||
|                         .map(csCount => { |                         .map(csCount => { | ||||||
|                             if (csCount < Constants.userJourney.personalLayoutUnlock) { |                             if (csCount < Constants.userJourney.personalLayoutUnlock) { | ||||||
|                                 return undefined |                                 return undefined | ||||||
|  | @ -91,7 +125,7 @@ export default class MoreScreen extends Combine { | ||||||
|     /* |     /* | ||||||
|     * Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets |     * Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets | ||||||
|     * */ |     * */ | ||||||
|     private static createCustomGeneratorButton(state: State): VariableUiElement { |     private static createCustomGeneratorButton(state: { osmConnection: OsmConnection }): VariableUiElement { | ||||||
|         const tr = Translations.t.general.morescreen; |         const tr = Translations.t.general.morescreen; | ||||||
|         return new VariableUiElement( |         return new VariableUiElement( | ||||||
|             state.osmConnection.userDetails.map(userDetails => { |             state.osmConnection.userDetails.map(userDetails => { | ||||||
|  | @ -111,13 +145,22 @@ export default class MoreScreen extends Combine { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Creates a button linking to the given theme |      * Creates a button linking to the given theme | ||||||
|      * @param layout |  | ||||||
|      * @param customThemeDefinition |  | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private static createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined): BaseUIElement { |     private static createLinkButton( | ||||||
|         if (layout === undefined) { |         state: { | ||||||
|             return undefined; |             locationControl?: UIEventSource<Loc>, | ||||||
|  |             layoutToUse?: LayoutConfig | ||||||
|  |         }, layout: LayoutConfig, customThemeDefinition: string = undefined | ||||||
|  |     ): | ||||||
|  |         BaseUIElement { | ||||||
|  |         if (layout | ||||||
|  | 
 | ||||||
|  |             === | ||||||
|  |             undefined | ||||||
|  |         ) { | ||||||
|  |             return | ||||||
|  |             undefined; | ||||||
|         } |         } | ||||||
|         if (layout.id === undefined) { |         if (layout.id === undefined) { | ||||||
|             console.error("ID is undefined for layout", layout); |             console.error("ID is undefined for layout", layout); | ||||||
|  | @ -126,11 +169,11 @@ export default class MoreScreen extends Combine { | ||||||
|         if (layout.hideFromOverview) { |         if (layout.hideFromOverview) { | ||||||
|             return undefined; |             return undefined; | ||||||
|         } |         } | ||||||
|         if (layout.id === State.state.layoutToUse?.id) { |         if (layout.id === state?.layoutToUse?.id) { | ||||||
|             return undefined; |             return undefined; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const currentLocation = State.state.locationControl; |         const currentLocation = state?.locationControl; | ||||||
| 
 | 
 | ||||||
|         let path = window.location.pathname; |         let path = window.location.pathname; | ||||||
|         // Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
 |         // Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
 | ||||||
|  | @ -151,7 +194,7 @@ export default class MoreScreen extends Combine { | ||||||
|             linkSuffix = `#${customThemeDefinition}` |             linkSuffix = `#${customThemeDefinition}` | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const linkText = currentLocation.map(currentLocation => { |         const linkText = currentLocation?.map(currentLocation => { | ||||||
|             const params = [ |             const params = [ | ||||||
|                 ["z", currentLocation?.zoom], |                 ["z", currentLocation?.zoom], | ||||||
|                 ["lat", currentLocation?.lat], |                 ["lat", currentLocation?.lat], | ||||||
|  | @ -160,7 +203,7 @@ export default class MoreScreen extends Combine { | ||||||
|                 .map(part => part[0] + "=" + part[1]) |                 .map(part => part[0] + "=" + part[1]) | ||||||
|                 .join("&") |                 .join("&") | ||||||
|             return `${linkPrefix}${params}${linkSuffix}`; |             return `${linkPrefix}${params}${linkSuffix}`; | ||||||
|         }) |         }) ?? new UIEventSource<string>(`${linkPrefix}${linkSuffix}`) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         let description = Translations.WT(layout.shortDescription).Clone(); |         let description = Translations.WT(layout.shortDescription).Clone(); | ||||||
|  |  | ||||||
|  | @ -2,38 +2,38 @@ import Combine from "../Base/Combine"; | ||||||
| import Toggle from "../Input/Toggle"; | import Toggle from "../Input/Toggle"; | ||||||
| import MapControlButton from "../MapControlButton"; | import MapControlButton from "../MapControlButton"; | ||||||
| import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"; | import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"; | ||||||
| import State from "../../State"; |  | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
|  | import MapState from "../../Logic/State/MapState"; | ||||||
| 
 | 
 | ||||||
| export default class RightControls extends Combine { | export default class RightControls extends Combine { | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor(state:MapState) { | ||||||
|         const geolocationButton = new Toggle( |         const geolocationButton = new Toggle( | ||||||
|             new MapControlButton( |             new MapControlButton( | ||||||
|                 new GeoLocationHandler( |                 new GeoLocationHandler( | ||||||
|                     State.state.currentGPSLocation, |                     state.currentGPSLocation, | ||||||
|                     State.state.leafletMap, |                     state.leafletMap, | ||||||
|                     State.state.layoutToUse |                     state.layoutToUse | ||||||
|                 ), { |                 ), { | ||||||
|                     dontStyle: true |                     dontStyle: true | ||||||
|                 } |                 } | ||||||
|             ), |             ), | ||||||
|             undefined, |             undefined, | ||||||
|             State.state.featureSwitchGeolocation |             state.featureSwitchGeolocation | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         const plus = new MapControlButton( |         const plus = new MapControlButton( | ||||||
|             Svg.plus_svg() |             Svg.plus_svg() | ||||||
|         ).onClick(() => { |         ).onClick(() => { | ||||||
|             State.state.locationControl.data.zoom++; |             state.locationControl.data.zoom++; | ||||||
|             State.state.locationControl.ping(); |             state.locationControl.ping(); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         const min = new MapControlButton( |         const min = new MapControlButton( | ||||||
|             Svg.min_svg() |             Svg.min_svg() | ||||||
|         ).onClick(() => { |         ).onClick(() => { | ||||||
|             State.state.locationControl.data.zoom--; |             state.locationControl.data.zoom--; | ||||||
|             State.state.locationControl.ping(); |             state.locationControl.ping(); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         super([plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1"))) |         super([plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1"))) | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import {Translation} from "../i18n/Translation"; | import {Translation} from "../i18n/Translation"; | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import State from "../../State"; |  | ||||||
| import {TextField} from "../Input/TextField"; | import {TextField} from "../Input/TextField"; | ||||||
| import {Geocoding} from "../../Logic/Osm/Geocoding"; | import {Geocoding} from "../../Logic/Osm/Geocoding"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
|  | @ -10,7 +9,10 @@ import Hash from "../../Logic/Web/Hash"; | ||||||
| import Combine from "../Base/Combine"; | import Combine from "../Base/Combine"; | ||||||
| 
 | 
 | ||||||
| export default class SearchAndGo extends Combine { | export default class SearchAndGo extends Combine { | ||||||
|     constructor() { |     constructor(state: { | ||||||
|  |         leafletMap: UIEventSource<any>, | ||||||
|  |         selectedElement: UIEventSource<any> | ||||||
|  |     }) { | ||||||
|         const goButton = Svg.search_ui().SetClass( |         const goButton = Svg.search_ui().SetClass( | ||||||
|             "w-8 h-8 full-rounded border-black float-right" |             "w-8 h-8 full-rounded border-black float-right" | ||||||
|         ); |         ); | ||||||
|  | @ -64,9 +66,9 @@ export default class SearchAndGo extends Combine { | ||||||
|                         [bb[0], bb[2]], |                         [bb[0], bb[2]], | ||||||
|                         [bb[1], bb[3]], |                         [bb[1], bb[3]], | ||||||
|                     ]; |                     ]; | ||||||
|                     State.state.selectedElement.setData(undefined); |                     state.selectedElement.setData(undefined); | ||||||
|                     Hash.hash.setData(poi.osm_type + "/" + poi.osm_id); |                     Hash.hash.setData(poi.osm_type + "/" + poi.osm_id); | ||||||
|                     State.state.leafletMap.data.fitBounds(bounds); |                     state.leafletMap.data.fitBounds(bounds); | ||||||
|                     placeholder.setData(Translations.t.general.search.search); |                     placeholder.setData(Translations.t.general.search.search); | ||||||
|                 }, |                 }, | ||||||
|                 () => { |                 () => { | ||||||
|  |  | ||||||
|  | @ -4,7 +4,6 @@ | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import {SubtleButton} from "../Base/SubtleButton"; | import {SubtleButton} from "../Base/SubtleButton"; | ||||||
| import State from "../../State"; |  | ||||||
| import Combine from "../Base/Combine"; | import Combine from "../Base/Combine"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants"; | ||||||
|  | @ -12,7 +11,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
| import Toggle from "../Input/Toggle"; | import Toggle from "../Input/Toggle"; | ||||||
| import UserDetails from "../../Logic/Osm/OsmConnection"; | import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||||
| import LocationInput from "../Input/LocationInput"; | import LocationInput from "../Input/LocationInput"; | ||||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||||
| import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; | import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; | ||||||
|  | @ -20,6 +19,11 @@ import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; | ||||||
| import PresetConfig from "../../Models/ThemeConfig/PresetConfig"; | import PresetConfig from "../../Models/ThemeConfig/PresetConfig"; | ||||||
| import FilteredLayer from "../../Models/FilteredLayer"; | import FilteredLayer from "../../Models/FilteredLayer"; | ||||||
| import {BBox} from "../../Logic/BBox"; | import {BBox} from "../../Logic/BBox"; | ||||||
|  | import Loc from "../../Models/Loc"; | ||||||
|  | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import {Changes} from "../../Logic/Osm/Changes"; | ||||||
|  | import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; | ||||||
|  | import {ElementStorage} from "../../Logic/ElementStorage"; | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
| * The SimpleAddUI is a single panel, which can have multiple states: | * The SimpleAddUI is a single panel, which can have multiple states: | ||||||
|  | @ -38,9 +42,22 @@ interface PresetInfo extends PresetConfig { | ||||||
| 
 | 
 | ||||||
| export default class SimpleAddUI extends Toggle { | export default class SimpleAddUI extends Toggle { | ||||||
| 
 | 
 | ||||||
|     constructor(isShown: UIEventSource<boolean>) { |     constructor(isShown: UIEventSource<boolean>, | ||||||
|  |                 filterViewIsOpened: UIEventSource<boolean>, | ||||||
|  |                 state: { | ||||||
|  |                     layoutToUse: LayoutConfig, | ||||||
|  |                     osmConnection: OsmConnection, | ||||||
|  |                     changes: Changes, | ||||||
|  |                     allElements: ElementStorage, | ||||||
|  |                     LastClickLocation: UIEventSource<{ lat: number, lon: number }>, | ||||||
|  |                     featurePipeline: FeaturePipeline, | ||||||
|  |                     selectedElement: UIEventSource<any>, | ||||||
|  |                     locationControl: UIEventSource<Loc>, | ||||||
|  |                     filteredLayers: UIEventSource<FilteredLayer[]>, | ||||||
|  |                     featureSwitchFilter: UIEventSource<boolean>, | ||||||
|  |                 }) { | ||||||
|         const loginButton = new SubtleButton(Svg.osm_logo_ui(), Translations.t.general.add.pleaseLogin.Clone()) |         const loginButton = new SubtleButton(Svg.osm_logo_ui(), Translations.t.general.add.pleaseLogin.Clone()) | ||||||
|             .onClick(() => State.state.osmConnection.AttemptLogin()); |             .onClick(() => state.osmConnection.AttemptLogin()); | ||||||
|         const readYourMessages = new Combine([ |         const readYourMessages = new Combine([ | ||||||
|             Translations.t.general.readYourMessages.Clone().SetClass("alert"), |             Translations.t.general.readYourMessages.Clone().SetClass("alert"), | ||||||
|             new SubtleButton(Svg.envelope_ui(), |             new SubtleButton(Svg.envelope_ui(), | ||||||
|  | @ -50,20 +67,21 @@ export default class SimpleAddUI extends Toggle { | ||||||
| 
 | 
 | ||||||
|         const selectedPreset = new UIEventSource<PresetInfo>(undefined); |         const selectedPreset = new UIEventSource<PresetInfo>(undefined); | ||||||
|         isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
 |         isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
 | ||||||
|         State.state.LastClickLocation.addCallback( _ => selectedPreset.setData(undefined)) |         state.LastClickLocation.addCallback(_ => selectedPreset.setData(undefined)) | ||||||
|          | 
 | ||||||
|         const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset) |         const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset, state) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|        async function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) { |         async function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) { | ||||||
|             const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { |             const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { | ||||||
|                 theme: State.state?.layoutToUse?.id ?? "unkown", |                 theme: state.layoutToUse?.id ?? "unkown", | ||||||
|                 changeType: "create", |                 changeType: "create", | ||||||
|                 snapOnto: snapOntoWay}) |                 snapOnto: snapOntoWay | ||||||
|             await State.state.changes.applyAction(newElementAction) |             }) | ||||||
|  |             await state.changes.applyAction(newElementAction) | ||||||
|             selectedPreset.setData(undefined) |             selectedPreset.setData(undefined) | ||||||
|             isShown.setData(false) |             isShown.setData(false) | ||||||
|             State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get( |             state.selectedElement.setData(state.allElements.ContainingFeatures.get( | ||||||
|                 newElementAction.newElementId |                 newElementAction.newElementId | ||||||
|             )) |             )) | ||||||
|         } |         } | ||||||
|  | @ -73,7 +91,7 @@ export default class SimpleAddUI extends Toggle { | ||||||
|                     if (preset === undefined) { |                     if (preset === undefined) { | ||||||
|                         return presetsOverview |                         return presetsOverview | ||||||
|                     } |                     } | ||||||
|                     return SimpleAddUI.CreateConfirmButton(preset, |                     return SimpleAddUI.CreateConfirmButton(state, filterViewIsOpened, preset, | ||||||
|                         (tags, location, snapOntoWayId?: string) => { |                         (tags, location, snapOntoWayId?: string) => { | ||||||
|                             if (snapOntoWayId === undefined) { |                             if (snapOntoWayId === undefined) { | ||||||
|                                 createNewPoint(tags, location, undefined) |                                 createNewPoint(tags, location, undefined) | ||||||
|  | @ -97,18 +115,18 @@ export default class SimpleAddUI extends Toggle { | ||||||
|                     new Toggle( |                     new Toggle( | ||||||
|                         addUi, |                         addUi, | ||||||
|                         Translations.t.general.add.stillLoading.Clone().SetClass("alert"), |                         Translations.t.general.add.stillLoading.Clone().SetClass("alert"), | ||||||
|                         State.state.featurePipeline.somethingLoaded |                         state.featurePipeline.somethingLoaded | ||||||
|                     ), |                     ), | ||||||
|                     Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"), |                     Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"), | ||||||
|                     State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints) |                     state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints) | ||||||
|                 ), |                 ), | ||||||
|                 readYourMessages, |                 readYourMessages, | ||||||
|                 State.state.osmConnection.userDetails.map((userdetails: UserDetails) => |                 state.osmConnection.userDetails.map((userdetails: UserDetails) => | ||||||
|                     userdetails.csCount >= Constants.userJourney.addNewPointWithUnreadMessagesUnlock || |                     userdetails.csCount >= Constants.userJourney.addNewPointWithUnreadMessagesUnlock || | ||||||
|                     userdetails.unreadMessages == 0) |                     userdetails.unreadMessages == 0) | ||||||
|             ), |             ), | ||||||
|             loginButton, |             loginButton, | ||||||
|             State.state.osmConnection.isLoggedIn |             state.osmConnection.isLoggedIn | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -116,11 +134,18 @@ export default class SimpleAddUI extends Toggle { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     private static CreateConfirmButton(preset: PresetInfo, |     private static CreateConfirmButton( | ||||||
|  |         state: { | ||||||
|  |           LastClickLocation: UIEventSource<{ lat: number, lon: number }>, | ||||||
|  |           osmConnection: OsmConnection, | ||||||
|  |           featurePipeline: FeaturePipeline   | ||||||
|  |         }, | ||||||
|  |             filterViewIsOpened: UIEventSource<boolean>, | ||||||
|  |         preset: PresetInfo, | ||||||
|                                        confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void, |                                        confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void, | ||||||
|                                        cancel: () => void): BaseUIElement { |                                        cancel: () => void): BaseUIElement { | ||||||
| 
 | 
 | ||||||
|         let location = State.state.LastClickLocation; |         let location = state.LastClickLocation; | ||||||
|         let preciseInput: LocationInput = undefined |         let preciseInput: LocationInput = undefined | ||||||
|         if (preset.preciseInput !== undefined) { |         if (preset.preciseInput !== undefined) { | ||||||
|             // We uncouple the event source
 |             // We uncouple the event source
 | ||||||
|  | @ -143,7 +168,6 @@ export default class SimpleAddUI extends Toggle { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|             const tags = TagUtils.KVtoProperties(preset.tags ?? []); |             const tags = TagUtils.KVtoProperties(preset.tags ?? []); | ||||||
|             preciseInput = new LocationInput({ |             preciseInput = new LocationInput({ | ||||||
|                 mapBackground: backgroundLayer, |                 mapBackground: backgroundLayer, | ||||||
|  | @ -160,24 +184,24 @@ export default class SimpleAddUI extends Toggle { | ||||||
|             if (preset.preciseInput.snapToLayers) { |             if (preset.preciseInput.snapToLayers) { | ||||||
|                 // We have to snap to certain layers.
 |                 // We have to snap to certain layers.
 | ||||||
|                 // Lets fetch them
 |                 // Lets fetch them
 | ||||||
|                  | 
 | ||||||
|                 let loadedBbox : BBox= undefined |                 let loadedBbox: BBox = undefined | ||||||
|                 mapBounds?.addCallbackAndRunD(bbox => { |                 mapBounds?.addCallbackAndRunD(bbox => { | ||||||
|                     if(loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)){ |                     if (loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)) { | ||||||
|                         // All is already there
 |                         // All is already there
 | ||||||
|                         // return;
 |                         // return;
 | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     bbox = bbox.pad(2); |                     bbox = bbox.pad(2); | ||||||
|                     loadedBbox = bbox; |                     loadedBbox = bbox; | ||||||
|                     const allFeatures: {feature: any}[] = [] |                     const allFeatures: { feature: any }[] = [] | ||||||
|                     preset.preciseInput.snapToLayers.forEach(layerId => { |                     preset.preciseInput.snapToLayers.forEach(layerId => { | ||||||
|                        State.state.featurePipeline.GetFeaturesWithin(layerId, bbox).forEach(feats => allFeatures.push(...feats.map(f => ({feature :f})))) |                         state.featurePipeline.GetFeaturesWithin(layerId, bbox).forEach(feats => allFeatures.push(...feats.map(f => ({feature: f})))) | ||||||
|                     }) |                     }) | ||||||
|                     snapToFeatures.setData(allFeatures) |                     snapToFeatures.setData(allFeatures) | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|              | 
 | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -205,7 +229,7 @@ export default class SimpleAddUI extends Toggle { | ||||||
|                     Translations.t.general.add.openLayerControl |                     Translations.t.general.add.openLayerControl | ||||||
|                 ]) |                 ]) | ||||||
|             ) |             ) | ||||||
|                 .onClick(() => State.state.filterIsOpened.setData(true)) |                 .onClick(() => filterViewIsOpened.setData(true)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         const openLayerOrConfirm = new Toggle( |         const openLayerOrConfirm = new Toggle( | ||||||
|  | @ -234,36 +258,35 @@ export default class SimpleAddUI extends Toggle { | ||||||
|             openLayerOrConfirm, |             openLayerOrConfirm, | ||||||
|             disableFilter, |             disableFilter, | ||||||
|             preset.layerToAddTo.appliedFilters.map(filters => { |             preset.layerToAddTo.appliedFilters.map(filters => { | ||||||
|                 if(filters === undefined || filters.length === 0){ |                 if (filters === undefined || filters.length === 0) { | ||||||
|                     return true; |                     return true; | ||||||
|                 } |                 } | ||||||
|                 for (const filter of filters) { |                 for (const filter of filters) { | ||||||
|                     if(filter.selected === 0 && filter.filter.options.length === 1){ |                     if (filter.selected === 0 && filter.filter.options.length === 1) { | ||||||
|                         return false; |                         return false; | ||||||
|                     } |                     } | ||||||
|                     if(filter.selected !== undefined){ |                     if (filter.selected !== undefined) { | ||||||
|                         const tags = filter.filter.options[filter.selected].osmTags |                         const tags = filter.filter.options[filter.selected].osmTags | ||||||
|                         if(tags !== undefined && tags["and"]?.length !== 0){ |                         if (tags !== undefined && tags["and"]?.length !== 0) { | ||||||
|                             // This actually doesn't filter anything at all
 |                             // This actually doesn't filter anything at all
 | ||||||
|                             return false; |                             return false; | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 return true |                 return true | ||||||
|                  | 
 | ||||||
|             }) |             }) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         const tagInfo = SimpleAddUI.CreateTagInfoFor(preset); |         const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection); | ||||||
| 
 | 
 | ||||||
|         const cancelButton = new SubtleButton(Svg.close_ui(), |         const cancelButton = new SubtleButton(Svg.close_ui(), | ||||||
|             Translations.t.general.cancel |             Translations.t.general.cancel | ||||||
|         ).onClick(cancel) |         ).onClick(cancel) | ||||||
| 
 | 
 | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|             // Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
 |             state.osmConnection.userDetails.data.dryRun ? | ||||||
|             State.state.osmConnection.userDetails.data.dryRun ? |  | ||||||
|                 Translations.t.general.testing.Clone().SetClass("alert") : undefined, |                 Translations.t.general.testing.Clone().SetClass("alert") : undefined, | ||||||
|             disableFiltersOrConfirm, |             disableFiltersOrConfirm, | ||||||
|             cancelButton, |             cancelButton, | ||||||
|  | @ -274,24 +297,29 @@ export default class SimpleAddUI extends Toggle { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static CreateTagInfoFor(preset: PresetInfo, optionallyLinkToWiki = true) { |     private static CreateTagInfoFor(preset: PresetInfo, osmConnection: OsmConnection, optionallyLinkToWiki = true) { | ||||||
|         const csCount = State.state.osmConnection.userDetails.data.csCount; |         const csCount = osmConnection.userDetails.data.csCount; | ||||||
|         return new Toggle( |         return new Toggle( | ||||||
|             Translations.t.general.add.presetInfo.Subs({ |             Translations.t.general.add.presetInfo.Subs({ | ||||||
|                 tags: preset.tags.map(t => t.asHumanString(optionallyLinkToWiki && csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"), |                 tags: preset.tags.map(t => t.asHumanString(optionallyLinkToWiki && csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"), | ||||||
|             }).SetStyle("word-break: break-all"), |             }).SetStyle("word-break: break-all"), | ||||||
| 
 | 
 | ||||||
|             undefined, |             undefined, | ||||||
|             State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt) |             osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt) | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement { |     private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>, | ||||||
|         const presetButtons = SimpleAddUI.CreatePresetButtons(selectedPreset) |                                          state: { | ||||||
|  |                                              filteredLayers: UIEventSource<FilteredLayer[]>, | ||||||
|  |                                              featureSwitchFilter: UIEventSource<boolean>, | ||||||
|  |                                              osmConnection: OsmConnection | ||||||
|  |                                          }): BaseUIElement { | ||||||
|  |         const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset) | ||||||
|         let intro: BaseUIElement = Translations.t.general.add.intro; |         let intro: BaseUIElement = Translations.t.general.add.intro; | ||||||
| 
 | 
 | ||||||
|         let testMode: BaseUIElement = undefined; |         let testMode: BaseUIElement = undefined; | ||||||
|         if (State.state.osmConnection?.userDetails?.data?.dryRun) { |         if (state.osmConnection?.userDetails?.data?.dryRun) { | ||||||
|             testMode = Translations.t.general.testing.Clone().SetClass("alert") |             testMode = Translations.t.general.testing.Clone().SetClass("alert") | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -299,9 +327,9 @@ export default class SimpleAddUI extends Toggle { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static CreatePresetSelectButton(preset: PresetInfo) { |     private static CreatePresetSelectButton(preset: PresetInfo, osmConnection: OsmConnection) { | ||||||
| 
 | 
 | ||||||
|         const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, false); |         const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, osmConnection ,false); | ||||||
|         return new SubtleButton( |         return new SubtleButton( | ||||||
|             preset.icon(), |             preset.icon(), | ||||||
|             new Combine([ |             new Combine([ | ||||||
|  | @ -316,11 +344,17 @@ export default class SimpleAddUI extends Toggle { | ||||||
| 
 | 
 | ||||||
|     /* |     /* | ||||||
|     * Generates the list with all the buttons.*/ |     * Generates the list with all the buttons.*/ | ||||||
|     private static CreatePresetButtons(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement { |     private static CreatePresetButtons( | ||||||
|  |         state: { | ||||||
|  |             filteredLayers: UIEventSource<FilteredLayer[]>, | ||||||
|  |             featureSwitchFilter: UIEventSource<boolean>, | ||||||
|  |             osmConnection: OsmConnection | ||||||
|  |         }, | ||||||
|  |         selectedPreset: UIEventSource<PresetInfo>): BaseUIElement { | ||||||
|         const allButtons = []; |         const allButtons = []; | ||||||
|         for (const layer of State.state.filteredLayers.data) { |         for (const layer of state.filteredLayers.data) { | ||||||
| 
 | 
 | ||||||
|             if (layer.isDisplayed.data === false && !State.state.featureSwitchFilter.data) { |             if (layer.isDisplayed.data === false && !state.featureSwitchFilter.data) { | ||||||
|                 // The layer is not displayed and we cannot enable the layer control -> we skip
 |                 // The layer is not displayed and we cannot enable the layer control -> we skip
 | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|  | @ -346,7 +380,7 @@ export default class SimpleAddUI extends Toggle { | ||||||
|                     preciseInput: preset.preciseInput |                     preciseInput: preset.preciseInput | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const button = SimpleAddUI.CreatePresetSelectButton(presetInfo); |                 const button = SimpleAddUI.CreatePresetSelectButton(presetInfo, state.osmConnection); | ||||||
|                 button.onClick(() => { |                 button.onClick(() => { | ||||||
|                     selectedPreset.setData(presetInfo) |                     selectedPreset.setData(presetInfo) | ||||||
|                 }) |                 }) | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import State from "../../State"; |  | ||||||
| import Combine from "../Base/Combine"; | import Combine from "../Base/Combine"; | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
| import LanguagePicker from "../LanguagePicker"; | import LanguagePicker from "../LanguagePicker"; | ||||||
|  | @ -8,24 +7,25 @@ import Translations from "../i18n/Translations"; | ||||||
| import Link from "../Base/Link"; | import Link from "../Base/Link"; | ||||||
| import Toggle from "../Input/Toggle"; | import Toggle from "../Input/Toggle"; | ||||||
| import Img from "../Base/Img"; | import Img from "../Base/Img"; | ||||||
|  | import MapState from "../../Logic/State/MapState"; | ||||||
| 
 | 
 | ||||||
| export default class UserBadge extends Toggle { | export default class UserBadge extends Toggle { | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor(state: MapState) { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         const userDetails = State.state.osmConnection.userDetails; |         const userDetails = state.osmConnection.userDetails; | ||||||
| 
 | 
 | ||||||
|         const loginButton = Translations.t.general.loginWithOpenStreetMap |         const loginButton = Translations.t.general.loginWithOpenStreetMap | ||||||
|             .Clone() |             .Clone() | ||||||
|             .SetClass("userbadge-login inline-flex justify-center items-center w-full h-full text-lg font-bold min-w-[20em]") |             .SetClass("userbadge-login inline-flex justify-center items-center w-full h-full text-lg font-bold min-w-[20em]") | ||||||
|             .onClick(() => State.state.osmConnection.AttemptLogin()); |             .onClick(() => state.osmConnection.AttemptLogin()); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         const logout = |         const logout = | ||||||
|             Svg.logout_svg() |             Svg.logout_svg() | ||||||
|                 .onClick(() => { |                 .onClick(() => { | ||||||
|                     State.state.osmConnection.LogOut(); |                     state.osmConnection.LogOut(); | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -39,15 +39,15 @@ export default class UserBadge extends Toggle { | ||||||
|                         return " "; |                         return " "; | ||||||
|                     }) |                     }) | ||||||
|                 ).onClick(() => { |                 ).onClick(() => { | ||||||
|                     const home = State.state.osmConnection.userDetails.data?.home; |                     const home = state.osmConnection.userDetails.data?.home; | ||||||
|                     if (home === undefined) { |                     if (home === undefined) { | ||||||
|                         return; |                         return; | ||||||
|                     } |                     } | ||||||
|                     State.state.leafletMap.data.setView([home.lat, home.lon], 16); |                     state.leafletMap.data.setView([home.lat, home.lon], 16); | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|                 const linkStyle = "flex items-baseline" |                 const linkStyle = "flex items-baseline" | ||||||
|                 const languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.language) ?? new FixedUiElement("")) |                 const languagePicker = (LanguagePicker.CreateLanguagePicker(state.layoutToUse.language) ?? new FixedUiElement("")) | ||||||
|                     .SetStyle("width:min-content;"); |                     .SetStyle("width:min-content;"); | ||||||
| 
 | 
 | ||||||
|                 let messageSpan = |                 let messageSpan = | ||||||
|  | @ -129,7 +129,7 @@ export default class UserBadge extends Toggle { | ||||||
|         super( |         super( | ||||||
|             userBadge, |             userBadge, | ||||||
|             loginButton, |             loginButton, | ||||||
|             State.state.osmConnection.isLoggedIn |             state.osmConnection.isLoggedIn | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,12 +1,11 @@ | ||||||
| import Translations from "./i18n/Translations"; | import Translations from "./i18n/Translations"; | ||||||
| import State from "../State"; |  | ||||||
| import {VariableUiElement} from "./Base/VariableUIElement"; | import {VariableUiElement} from "./Base/VariableUIElement"; | ||||||
|  | import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; | ||||||
| 
 | 
 | ||||||
| export default class CenterMessageBox extends VariableUiElement { | export default class CenterMessageBox extends VariableUiElement { | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor(state: FeaturePipelineState) { | ||||||
|         const state = State.state; |         const updater = state.featurePipeline; | ||||||
|         const updater = State.state.featurePipeline; |  | ||||||
|         const t = Translations.t.centerMessage; |         const t = Translations.t.centerMessage; | ||||||
|         const message = updater.runningQuery.map( |         const message = updater.runningQuery.map( | ||||||
|             isRunning => { |             isRunning => { | ||||||
|  |  | ||||||
							
								
								
									
										161
									
								
								UI/DefaultGUI.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								UI/DefaultGUI.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,161 @@ | ||||||
|  | import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; | ||||||
|  | import State from "../State"; | ||||||
|  | import {Utils} from "../Utils"; | ||||||
|  | import {UIEventSource} from "../Logic/UIEventSource"; | ||||||
|  | import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs"; | ||||||
|  | import MapControlButton from "./MapControlButton"; | ||||||
|  | import Svg from "../Svg"; | ||||||
|  | import Toggle from "./Input/Toggle"; | ||||||
|  | import Hash from "../Logic/Web/Hash"; | ||||||
|  | import {QueryParameters} from "../Logic/Web/QueryParameters"; | ||||||
|  | import Constants from "../Models/Constants"; | ||||||
|  | import UserBadge from "./BigComponents/UserBadge"; | ||||||
|  | import SearchAndGo from "./BigComponents/SearchAndGo"; | ||||||
|  | import Link from "./Base/Link"; | ||||||
|  | import BaseUIElement from "./BaseUIElement"; | ||||||
|  | import {VariableUiElement} from "./Base/VariableUIElement"; | ||||||
|  | import LeftControls from "./BigComponents/LeftControls"; | ||||||
|  | import RightControls from "./BigComponents/RightControls"; | ||||||
|  | import CenterMessageBox from "./CenterMessageBox"; | ||||||
|  | 
 | ||||||
|  | export class DefaultGuiState { | ||||||
|  |     public readonly welcomeMessageIsOpened; | ||||||
|  |     public readonly downloadControlIsOpened: UIEventSource<boolean>; | ||||||
|  |     public readonly filterViewIsOpened: UIEventSource<boolean>; | ||||||
|  |     public readonly welcomeMessageOpenedTab | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         this.filterViewIsOpened = QueryParameters.GetQueryParameter( | ||||||
|  |             "filter-toggle", | ||||||
|  |             "false", | ||||||
|  |             "Whether or not the filter view is shown" | ||||||
|  |         ).map<boolean>( | ||||||
|  |             (str) => str !== "false", | ||||||
|  |             [], | ||||||
|  |             (b) => "" + b | ||||||
|  |         ); | ||||||
|  |         this.welcomeMessageIsOpened = new UIEventSource<boolean>(Hash.hash.data === undefined || | ||||||
|  |             Hash.hash.data === "" || | ||||||
|  |             Hash.hash.data == "welcome"); | ||||||
|  | 
 | ||||||
|  |         this.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 | ||||||
|  |         ); | ||||||
|  |         this.downloadControlIsOpened = | ||||||
|  |             QueryParameters.GetQueryParameter( | ||||||
|  |                 "download-control-toggle", | ||||||
|  |                 "false", | ||||||
|  |                 "Whether or not the download panel is shown" | ||||||
|  |             ).map<boolean>( | ||||||
|  |                 (str) => str !== "false", | ||||||
|  |                 [], | ||||||
|  |                 (b) => "" + b | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * The default MapComplete GUI initializor | ||||||
|  |  * | ||||||
|  |  * Adds a welcome pane, contorl buttons, ... etc to index.html | ||||||
|  |  */ | ||||||
|  | export default class DefaultGUI { | ||||||
|  |     private readonly _guiState: DefaultGuiState; | ||||||
|  |     private readonly state: FeaturePipelineState; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { | ||||||
|  |         this.state = state; | ||||||
|  |         this._guiState = guiState; | ||||||
|  |         const self = this; | ||||||
|  | 
 | ||||||
|  |         if (state.layoutToUse.customCss !== undefined) { | ||||||
|  |             Utils.LoadCustomCss(state.layoutToUse.customCss); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Attach the map
 | ||||||
|  |         state.mainMapObject.SetClass("w-full h-full") | ||||||
|  |             .AttachTo("leafletDiv") | ||||||
|  | 
 | ||||||
|  |         state.setupClickDialogOnMap( | ||||||
|  |             guiState.filterViewIsOpened, | ||||||
|  |             state.leafletMap | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         this.InitWelcomeMessage(); | ||||||
|  | 
 | ||||||
|  |         Toggle.If(state.featureSwitchUserbadge, | ||||||
|  |             () => new UserBadge(state) | ||||||
|  |         ).AttachTo("userbadge") | ||||||
|  | 
 | ||||||
|  |         Toggle.If(state.featureSwitchSearch, | ||||||
|  |             () => new SearchAndGo(state)) | ||||||
|  |             .AttachTo("searchbox"); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         let iframePopout: () => BaseUIElement = undefined; | ||||||
|  | 
 | ||||||
|  |         if (window !== window.top) { | ||||||
|  |             // MapComplete is running in an iframe
 | ||||||
|  |             iframePopout = () => new VariableUiElement(state.locationControl.map(loc => { | ||||||
|  |                 const url = `${window.location.origin}${window.location.pathname}?z=${loc.zoom ?? 0}&lat=${loc.lat ?? 0}&lon=${loc.lon ?? 0}`; | ||||||
|  |                 const link = new Link(Svg.pop_out_img, url, true).SetClass("block w-full h-full p-1.5") | ||||||
|  |                 return new MapControlButton(link) | ||||||
|  |             })) | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         new Toggle(self.InitWelcomeMessage(), | ||||||
|  |             Toggle.If(state.featureSwitchIframePopoutEnabled,  iframePopout), | ||||||
|  |             state.featureSwitchWelcomeMessage | ||||||
|  |         ).AttachTo("messagesbox"); | ||||||
|  | 
 | ||||||
|  |         new LeftControls(state, guiState).AttachTo("bottom-left"); | ||||||
|  |         new RightControls(state).AttachTo("bottom-right"); | ||||||
|  |         State.state.locationControl.ping(); | ||||||
|  |         new CenterMessageBox(state).AttachTo("centermessage"); | ||||||
|  |         document | ||||||
|  |             .getElementById("centermessage") | ||||||
|  |             .classList.add("pointer-events-none"); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private InitWelcomeMessage() { | ||||||
|  |         const isOpened = this._guiState.welcomeMessageIsOpened | ||||||
|  |         const fullOptions = new FullWelcomePaneWithTabs(isOpened, this._guiState.welcomeMessageOpenedTab, this.state); | ||||||
|  | 
 | ||||||
|  |         // ?-Button on Desktop, opens panel with close-X.
 | ||||||
|  |         const help = new MapControlButton(Svg.help_svg()); | ||||||
|  |         help.onClick(() => isOpened.setData(true)); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const openedTime = new Date().getTime(); | ||||||
|  |         this.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); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         this.state.selectedElement.addCallbackAndRunD((_) => { | ||||||
|  |             isOpened.setData(false); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         return new Toggle( | ||||||
|  |             fullOptions.SetClass("welcomeMessage pointer-events-auto"), | ||||||
|  |             help.SetClass("pointer-events-auto"), | ||||||
|  |             isOpened | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -104,19 +104,7 @@ export default class ExportPDF { | ||||||
|              |              | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const initialized =new Set() |         State.state.AddAllOverlaysToMap(minimap.leafletMap) | ||||||
|         for (const overlayToggle of State.state.overlayToggles) { |  | ||||||
|             new ShowOverlayLayer(overlayToggle.config, minimap.leafletMap, overlayToggle.isDisplayed) |  | ||||||
|             initialized.add(overlayToggle.config) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (const tileLayerSource of State.state.layoutToUse.tileLayerSources) { |  | ||||||
|             if (initialized.has(tileLayerSource)) { |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
|             new ShowOverlayLayer(tileLayerSource, minimap.leafletMap) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private cleanup() { |     private cleanup() { | ||||||
|  |  | ||||||
|  | @ -176,7 +176,8 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO | ||||||
|                         enablePopups: false, |                         enablePopups: false, | ||||||
|                         zoomToFeatures: false, |                         zoomToFeatures: false, | ||||||
|                         leafletMap: this.map.leafletMap, |                         leafletMap: this.map.leafletMap, | ||||||
|                         layers: State.state.filteredLayers |                         layers: State.state.filteredLayers, | ||||||
|  |                     allElements: State.state.allElements | ||||||
|                     } |                     } | ||||||
|                 ) |                 ) | ||||||
|                 // Show the central point
 |                 // Show the central point
 | ||||||
|  | @ -191,7 +192,9 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO | ||||||
|                         enablePopups: false, |                         enablePopups: false, | ||||||
|                         zoomToFeatures: false, |                         zoomToFeatures: false, | ||||||
|                         leafletMap: this.map.leafletMap, |                         leafletMap: this.map.leafletMap, | ||||||
|                         layerToShow: this._matching_layer |                         layerToShow: this._matching_layer, | ||||||
|  |                         allElements: State.state.allElements, | ||||||
|  |                         selectedElement: State.state.selectedElement | ||||||
|                     }) |                     }) | ||||||
|                      |                      | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
|  | import Lazy from "../Base/Lazy"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The 'Toggle' is a UIElement showing either one of two elements, depending on the state. |  * The 'Toggle' is a UIElement showing either one of two elements, depending on the state. | ||||||
|  | @ -24,4 +25,16 @@ export default class Toggle extends VariableUiElement { | ||||||
|         }) |         }) | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |    public static If(condition: UIEventSource<boolean>, constructor: () => BaseUIElement): BaseUIElement { | ||||||
|  |         if(constructor === undefined){ | ||||||
|  |             return undefined | ||||||
|  |         } | ||||||
|  |         return new Toggle( | ||||||
|  |             new Lazy(constructor), | ||||||
|  |             undefined, | ||||||
|  |             condition | ||||||
|  |         ) | ||||||
|  |          | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -68,7 +68,7 @@ export default class SplitRoadWizard extends Toggle { | ||||||
|             leafletMap: miniMap.leafletMap, |             leafletMap: miniMap.leafletMap, | ||||||
|             zoomToFeatures: false, |             zoomToFeatures: false, | ||||||
|             enablePopups: false, |             enablePopups: false, | ||||||
|             layerToShow: SplitRoadWizard.splitLayerStyling |             layerToShow: SplitRoadWizard.splitLayerStyling, | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         new ShowDataMultiLayer({ |         new ShowDataMultiLayer({ | ||||||
|  | @ -76,7 +76,8 @@ export default class SplitRoadWizard extends Toggle { | ||||||
|             layers: State.state.filteredLayers, |             layers: State.state.filteredLayers, | ||||||
|             leafletMap: miniMap.leafletMap, |             leafletMap: miniMap.leafletMap, | ||||||
|             enablePopups: false, |             enablePopups: false, | ||||||
|             zoomToFeatures: true |             zoomToFeatures: true, | ||||||
|  |             allElements: State.state.allElements, | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|  |  | ||||||
|  | @ -4,9 +4,9 @@ | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||||
| import FeatureInfoBox from "../Popup/FeatureInfoBox"; | import FeatureInfoBox from "../Popup/FeatureInfoBox"; | ||||||
| import State from "../../State"; |  | ||||||
| import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; | import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | import {ElementStorage} from "../../Logic/ElementStorage"; | ||||||
|  | import Hash from "../../Logic/Web/Hash"; | ||||||
| 
 | 
 | ||||||
| export default class ShowDataLayer { | export default class ShowDataLayer { | ||||||
| 
 | 
 | ||||||
|  | @ -14,7 +14,8 @@ export default class ShowDataLayer { | ||||||
|     private readonly _enablePopups: boolean; |     private readonly _enablePopups: boolean; | ||||||
|     private readonly _features: UIEventSource<{ feature: any }[]> |     private readonly _features: UIEventSource<{ feature: any }[]> | ||||||
|     private readonly _layerToShow: LayerConfig; |     private readonly _layerToShow: LayerConfig; | ||||||
| 
 |     private readonly _selectedElement: UIEventSource<any> | ||||||
|  |     private readonly allElements : ElementStorage | ||||||
|     // Used to generate a fresh ID when needed
 |     // Used to generate a fresh ID when needed
 | ||||||
|     private _cleanCount = 0; |     private _cleanCount = 0; | ||||||
|     private geoLayer = undefined; |     private geoLayer = undefined; | ||||||
|  | @ -43,6 +44,8 @@ export default class ShowDataLayer { | ||||||
|         const features = options.features.features.map(featFreshes => featFreshes.map(ff => ff.feature)); |         const features = options.features.features.map(featFreshes => featFreshes.map(ff => ff.feature)); | ||||||
|         this._features = features; |         this._features = features; | ||||||
|         this._layerToShow = options.layerToShow; |         this._layerToShow = options.layerToShow; | ||||||
|  |         this._selectedElement = options.selectedElement | ||||||
|  |         this.allElements = options.allElements; | ||||||
|         const self = this; |         const self = this; | ||||||
| 
 | 
 | ||||||
|         options.leafletMap.addCallbackAndRunD(_ => { |         options.leafletMap.addCallbackAndRunD(_ => { | ||||||
|  | @ -71,7 +74,7 @@ export default class ShowDataLayer { | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         State.state.selectedElement.addCallbackAndRunD(selected => { |         this._selectedElement?.addCallbackAndRunD(selected => { | ||||||
|             if (self._leafletMap.data === undefined) { |             if (self._leafletMap.data === undefined) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  | @ -162,7 +165,7 @@ export default class ShowDataLayer { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     private createStyleFor(feature) { |     private createStyleFor(feature) { | ||||||
|         const tagsSource = State.state.allElements.addOrGetElement(feature); |         const tagsSource = this.allElements?.addOrGetElement(feature) ?? new UIEventSource<any>(feature.properties.id); | ||||||
|         // Every object is tied to exactly one layer
 |         // Every object is tied to exactly one layer
 | ||||||
|         const layer = this._layerToShow |         const layer = this._layerToShow | ||||||
|         return layer?.GenerateLeafletStyle(tagsSource, true); |         return layer?.GenerateLeafletStyle(tagsSource, true); | ||||||
|  | @ -178,10 +181,7 @@ export default class ShowDataLayer { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let tagSource = State.state.allElements.getEventSourceById(feature.properties.id) |         let tagSource = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource<any>(feature.properties) | ||||||
|         if (tagSource === undefined) { |  | ||||||
|             tagSource = new UIEventSource<any>(feature.properties) |  | ||||||
|         } |  | ||||||
|         const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0) |         const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0) | ||||||
|         const style = layer.GenerateLeafletStyle(tagSource, clickable); |         const style = layer.GenerateLeafletStyle(tagSource, clickable); | ||||||
|         const baseElement = style.icon.html; |         const baseElement = style.icon.html; | ||||||
|  | @ -230,20 +230,20 @@ export default class ShowDataLayer { | ||||||
|         popup.setContent(`<div style='height: 65vh' id='${id}'>Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading</div>`) |         popup.setContent(`<div style='height: 65vh' id='${id}'>Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading</div>`) | ||||||
|         leafletLayer.on("popupopen", () => { |         leafletLayer.on("popupopen", () => { | ||||||
|             if (infobox === undefined) { |             if (infobox === undefined) { | ||||||
|                 const tags = State.state.allElements.getEventSourceById(feature.properties.id); |                 const tags = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource<any>(feature.properties); | ||||||
|                 infobox = new FeatureInfoBox(tags, layer); |                 infobox = new FeatureInfoBox(tags, layer); | ||||||
| 
 | 
 | ||||||
|                 infobox.isShown.addCallback(isShown => { |                 infobox.isShown.addCallback(isShown => { | ||||||
|                     if (!isShown) { |                     if (!isShown) { | ||||||
|                         State.state.selectedElement.setData(undefined); |                         this._selectedElement?.setData(undefined); | ||||||
|                         leafletLayer.closePopup() |                         leafletLayer.closePopup() | ||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|             infobox.AttachTo(id) |             infobox.AttachTo(id) | ||||||
|             infobox.Activate(); |             infobox.Activate(); | ||||||
|             if (State.state?.selectedElement?.data?.properties?.id !== feature.properties.id) { |             if (this._selectedElement?.data?.properties?.id !== feature.properties.id) { | ||||||
|                 State.state.selectedElement.setData(feature) |                 this._selectedElement?.setData(feature) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|         }); |         }); | ||||||
|  | @ -254,6 +254,7 @@ export default class ShowDataLayer { | ||||||
|             feature: feature, |             feature: feature, | ||||||
|             leafletlayer: leafletLayer |             leafletlayer: leafletLayer | ||||||
|         }) |         }) | ||||||
|  |          | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,11 @@ | ||||||
| import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; | import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
|  | import {ElementStorage} from "../../Logic/ElementStorage"; | ||||||
| 
 | 
 | ||||||
| export interface ShowDataLayerOptions { | export interface ShowDataLayerOptions { | ||||||
|     features: FeatureSource, |     features: FeatureSource, | ||||||
|  |     selectedElement?: UIEventSource<any>, | ||||||
|  |     allElements?: ElementStorage, | ||||||
|     leafletMap: UIEventSource<L.Map>, |     leafletMap: UIEventSource<L.Map>, | ||||||
|     enablePopups?: true | boolean, |     enablePopups?: true | boolean, | ||||||
|     zoomToFeatures?: false | boolean, |     zoomToFeatures?: false | boolean, | ||||||
|  |  | ||||||
|  | @ -208,7 +208,8 @@ export default class SpecialVisualizations { | ||||||
|                             enablePopups: false, |                             enablePopups: false, | ||||||
|                             zoomToFeatures: true, |                             zoomToFeatures: true, | ||||||
|                             layers: State.state.filteredLayers, |                             layers: State.state.filteredLayers, | ||||||
|                             features: new StaticFeatureSource(featuresToShow, true) |                             features: new StaticFeatureSource(featuresToShow, true), | ||||||
|  |                             allElements: State.state.allElements | ||||||
|                         } |                         } | ||||||
|                     ) |                     ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										13
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -463,5 +463,18 @@ export class Utils { | ||||||
|         } |         } | ||||||
|             return hours+":"+Utils.TwoDigits(minutes)+":"+Utils.TwoDigits(seconds) |             return hours+":"+Utils.TwoDigits(minutes)+":"+Utils.TwoDigits(seconds) | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     public static DisableLongPresses(){ | ||||||
|  |         // Remove all context event listeners on mobile to prevent long presses
 | ||||||
|  |         window.addEventListener('contextmenu', (e) => { // Not compatible with IE < 9
 | ||||||
|  | 
 | ||||||
|  |             if (e.target["nodeName"] === "INPUT") { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             e.preventDefault(); | ||||||
|  |             return false; | ||||||
|  |         }, false); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -263,5 +263,6 @@ | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   ] |   ], | ||||||
|  |   "enableIframePopout": false | ||||||
| } | } | ||||||
|  | @ -45,6 +45,7 @@ | ||||||
|   "startLon": 0, |   "startLon": 0, | ||||||
|   "startZoom": 16, |   "startZoom": 16, | ||||||
|   "widenFactor": 1.2, |   "widenFactor": 1.2, | ||||||
|  |   "overpassMaxZoom": 0, | ||||||
|   "layers": [], |   "layers": [], | ||||||
|   "roamingRenderings": [] |   "roamingRenderings": [] | ||||||
| } | } | ||||||
							
								
								
									
										176
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										176
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -1,27 +1,24 @@ | ||||||
| import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; |  | ||||||
| import {FixedUiElement} from "./UI/Base/FixedUiElement"; | import {FixedUiElement} from "./UI/Base/FixedUiElement"; | ||||||
| import {InitUiElements} from "./InitUiElements"; |  | ||||||
| import {QueryParameters} from "./Logic/Web/QueryParameters"; | import {QueryParameters} from "./Logic/Web/QueryParameters"; | ||||||
| import {UIEventSource} from "./Logic/UIEventSource"; |  | ||||||
| import * as $ from "jquery"; |  | ||||||
| import MoreScreen from "./UI/BigComponents/MoreScreen"; |  | ||||||
| import State from "./State"; |  | ||||||
| import Combine from "./UI/Base/Combine"; | import Combine from "./UI/Base/Combine"; | ||||||
| import Translations from "./UI/i18n/Translations"; |  | ||||||
| import ValidatedTextField from "./UI/Input/ValidatedTextField"; | import ValidatedTextField from "./UI/Input/ValidatedTextField"; | ||||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | ||||||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; |  | ||||||
| import Constants from "./Models/Constants"; |  | ||||||
| import MinimapImplementation from "./UI/Base/MinimapImplementation"; | import MinimapImplementation from "./UI/Base/MinimapImplementation"; | ||||||
| import CountryCoder from "latlon2country/index"; | import CountryCoder from "latlon2country/index"; | ||||||
| import SimpleMetaTagger from "./Logic/SimpleMetaTagger"; | import SimpleMetaTagger from "./Logic/SimpleMetaTagger"; | ||||||
|  | import {Utils} from "./Utils"; | ||||||
|  | import AllThemesGui from "./UI/AllThemesGui"; | ||||||
|  | import DetermineLayout from "./Logic/DetermineLayout"; | ||||||
|  | import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import DefaultGUI, {DefaultGuiState} from "./UI/DefaultGUI"; | ||||||
|  | import State from "./State"; | ||||||
| 
 | 
 | ||||||
| MinimapImplementation.initialize() | MinimapImplementation.initialize() | ||||||
| // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
 | // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
 | ||||||
| ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref) | ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref) | ||||||
| SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | ||||||
|  | Utils.DisableLongPresses() | ||||||
| 
 | 
 | ||||||
| let defaultLayout = "" |  | ||||||
| // --------------------- Special actions based on the parameters -----------------
 | // --------------------- Special actions based on the parameters -----------------
 | ||||||
| // @ts-ignore
 | // @ts-ignore
 | ||||||
| if (location.href.startsWith("http://buurtnatuur.be")) { | if (location.href.startsWith("http://buurtnatuur.be")) { | ||||||
|  | @ -30,60 +27,65 @@ if (location.href.startsWith("http://buurtnatuur.be")) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| if (location.href.indexOf("buurtnatuur.be") >= 0) { | class Init { | ||||||
|     defaultLayout = "buurtnatuur" |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| let testing: UIEventSource<string>; |     public static Init(layoutToUse: LayoutConfig, encoded: string) { | ||||||
| if (QueryParameters.GetQueryParameter("backend", undefined).data !== "osm-test" && | 
 | ||||||
|     (location.hostname === "localhost" || location.hostname === "127.0.0.1")) { |         if(layoutToUse === null){ | ||||||
|     testing = QueryParameters.GetQueryParameter("test", "true"); |             // Something went wrong, error message is already on screen
 | ||||||
|     // Set to true if testing and changes should NOT be saved
 |             return; | ||||||
|     testing.setData(testing.data ?? "true") |         } | ||||||
|     // If you have a testfile somewhere, enable this to spoof overpass
 |          | ||||||
|     // This should be hosted independantly, e.g. with `cd assets; webfsd -p 8080` + a CORS plugin to disable cors rules
 |         if (layoutToUse === undefined) { | ||||||
|     // Overpass.testUrl = "http://127.0.0.1:8080/streetwidths.geojson";
 |             // No layout found
 | ||||||
| } else { |             new AllThemesGui() | ||||||
|     testing = QueryParameters.GetQueryParameter("test", "false"); |             return; | ||||||
| } |         } | ||||||
|  | 
 | ||||||
|  |         // Workaround/legacy to keep the old paramters working as I renamed some of them
 | ||||||
|  |         if (layoutToUse?.id === "cyclofix") { | ||||||
|  |             const legacy = QueryParameters.GetQueryParameter("layer-bike_shops", "true", "Legacy - keep De Fietsambassade working"); | ||||||
|  |             const correct = QueryParameters.GetQueryParameter("layer-bike_shop", "true", "Legacy - keep De Fietsambassade working") | ||||||
|  |             if (legacy.data !== "true") { | ||||||
|  |                 correct.setData(legacy.data) | ||||||
|  |             } | ||||||
|  |             console.log("layer-bike_shop toggles: legacy:", legacy.data, "new:", correct.data) | ||||||
|  | 
 | ||||||
|  |             const legacyCafe = QueryParameters.GetQueryParameter("layer-bike_cafes", "true", "Legacy - keep De Fietsambassade working") | ||||||
|  |             const correctCafe = QueryParameters.GetQueryParameter("layer-bike_cafe", "true", "Legacy - keep De Fietsambassade working") | ||||||
|  |             if (legacyCafe.data !== "true") { | ||||||
|  |                 correctCafe.setData(legacy.data) | ||||||
|  |             } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| // ----------------- SELECT THE RIGHT Theme -----------------
 |         const guiState = new DefaultGuiState() | ||||||
|  |         State.state = new State(layoutToUse); | ||||||
|  |         // This 'leaks' the global state via the window object, useful for debugging
 | ||||||
|  |         // @ts-ignore
 | ||||||
|  |         window.mapcomplete_state = State.state; | ||||||
|  |         new DefaultGUI(State.state, guiState) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         if (encoded !== undefined && encoded.length > 10) { | ||||||
|  |             // We save the layout to the user settings and local storage
 | ||||||
|  |             State.state.osmConnection.OnLoggedIn(() => { | ||||||
|  |                 State.state.osmConnection | ||||||
|  |                     .GetLongPreference("installed-theme-" + layoutToUse.id) | ||||||
|  |                     .setData(encoded); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const path = window.location.pathname.split("/").slice(-1)[0]; |  | ||||||
| if (path !== "index.html" && path !== "") { |  | ||||||
|     defaultLayout = path; |  | ||||||
|     if (path.endsWith(".html")) { |  | ||||||
|         defaultLayout = path.substr(0, path.length - 5); |  | ||||||
|     } |     } | ||||||
|     console.log("Using layout", defaultLayout); |  | ||||||
| } |  | ||||||
| defaultLayout = QueryParameters.GetQueryParameter("layout", defaultLayout, "The layout to load into MapComplete").data; |  | ||||||
| let layoutToUse: LayoutConfig = AllKnownLayouts.allKnownLayouts.get(defaultLayout.toLowerCase()); |  | ||||||
| 
 | 
 | ||||||
| const userLayoutParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme"); |  | ||||||
| 
 | 
 | ||||||
| // Workaround/legacy to keep the old paramters working as I renamed some of them
 |  | ||||||
| if (layoutToUse?.id === "cyclofix") { |  | ||||||
|     const legacy = QueryParameters.GetQueryParameter("layer-bike_shops", "true", "Legacy - keep De Fietsambassade working"); |  | ||||||
|     const correct = QueryParameters.GetQueryParameter("layer-bike_shop", "true", "Legacy - keep De Fietsambassade working") |  | ||||||
|     if (legacy.data !== "true") { |  | ||||||
|         correct.setData(legacy.data) |  | ||||||
|     } |  | ||||||
|     console.log("layer-bike_shop toggles: legacy:", legacy.data, "new:", correct.data) |  | ||||||
| 
 |  | ||||||
|     const legacyCafe = QueryParameters.GetQueryParameter("layer-bike_cafes", "true", "Legacy - keep De Fietsambassade working") |  | ||||||
|     const correctCafe = QueryParameters.GetQueryParameter("layer-bike_cafe", "true", "Legacy - keep De Fietsambassade working") |  | ||||||
|     if (legacyCafe.data !== "true") { |  | ||||||
|         correctCafe.setData(legacy.data) |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const layoutFromBase64 = decodeURIComponent(userLayoutParam.data); | document.getElementById("decoration-desktop").remove(); | ||||||
| 
 |  | ||||||
| new Combine(["Initializing... <br/>", | new Combine(["Initializing... <br/>", | ||||||
|     new FixedUiElement("<a>If this message persist, something went wrong - click here to try again</a>") |     new FixedUiElement("<a>If this message persist, something went wrong - click here to try again</a>") | ||||||
|         .SetClass("link-underline small") |         .SetClass("link-underline small") | ||||||
|  | @ -93,71 +95,13 @@ new Combine(["Initializing... <br/>", | ||||||
| 
 | 
 | ||||||
|         })]) |         })]) | ||||||
|     .AttachTo("centermessage"); // Add an initialization and reset button if something goes wrong
 |     .AttachTo("centermessage"); // Add an initialization and reset button if something goes wrong
 | ||||||
| document.getElementById("decoration-desktop").remove(); |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| if (layoutFromBase64.startsWith("http")) { | DetermineLayout.GetLayout().then(value => { | ||||||
|     const link = layoutFromBase64; |     console.log("Got ", value) | ||||||
|     console.log("Downloading map theme from ", link); |     Init.Init(value[0], value[1]) | ||||||
|     new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`) | }).catch(err => { | ||||||
|         .AttachTo("centermessage"); |     console.error(err) | ||||||
|  | }) | ||||||
| 
 | 
 | ||||||
|     $.ajax({ |  | ||||||
|         url: link, |  | ||||||
|         success: (data) => { |  | ||||||
| 
 | 
 | ||||||
|             try { |  | ||||||
|                 console.log("Received ", data) |  | ||||||
|                 let parsed = data; |  | ||||||
|                 if (typeof parsed == "string") { |  | ||||||
|                     parsed = JSON.parse(data); |  | ||||||
|                 } else { |  | ||||||
|                     data = JSON.stringify(parsed) // De wereld op zijn kop
 |  | ||||||
|                 } |  | ||||||
|                 // Overwrite the id to the wiki:value
 |  | ||||||
|                 parsed.id = link; |  | ||||||
|                 const layout = new LayoutConfig(parsed, false).patchImages(link, data); |  | ||||||
|                 InitUiElements.InitAll(layout, layoutFromBase64, testing, layoutFromBase64, btoa(data)); |  | ||||||
|             } catch (e) { |  | ||||||
|                 new FixedUiElement(`<a href="${link}">${link}</a> is invalid:<br/>${e}<br/> <a href='https://${window.location.host}/'>Go back</a>`) |  | ||||||
|                     .SetClass("clickable") |  | ||||||
|                     .AttachTo("centermessage"); |  | ||||||
|                 console.error("Could not parse the text", data) |  | ||||||
|                 throw e; |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|     }).fail((_, textstatus, error) => { |  | ||||||
|         console.error("Could not download the wiki theme:", textstatus, error) |  | ||||||
|         new FixedUiElement(`<a href="${link}">${link}</a> is invalid:<br/>Could not download - wrong URL?<br/>` + |  | ||||||
|             error + |  | ||||||
|             "<a href='https://${window.location.host}/'>Go back</a>") |  | ||||||
|             .SetClass("clickable") |  | ||||||
|             .AttachTo("centermessage"); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
| } else if (layoutFromBase64 !== "false") { |  | ||||||
|     let [layoutToUse, encoded] = InitUiElements.LoadLayoutFromHash(userLayoutParam); |  | ||||||
|     InitUiElements.InitAll(layoutToUse, layoutFromBase64, testing, defaultLayout, encoded); |  | ||||||
| } else if (layoutToUse !== undefined) { |  | ||||||
|     // This is the default case: a builtin theme
 |  | ||||||
|     InitUiElements.InitAll(layoutToUse, layoutFromBase64, testing, defaultLayout); |  | ||||||
| } else { |  | ||||||
|     // We fall through: no theme loaded: just show an overview of layouts
 |  | ||||||
|     new FixedUiElement("").AttachTo("centermessage") |  | ||||||
|     State.state = new State(undefined); |  | ||||||
|     new Combine([new MoreScreen(true), |  | ||||||
|         Translations.t.general.aboutMapcomplete.SetClass("link-underline"), |  | ||||||
|         new FixedUiElement("v" + Constants.vNumber) |  | ||||||
|     ]).SetClass("block m-5 lg:w-3/4 lg:ml-40") |  | ||||||
|         .SetStyle("pointer-events: all;") |  | ||||||
|         .AttachTo("topleft-tools"); |  | ||||||
| } |  | ||||||
| // Remove all context event listeners on mobile to prevent long presses
 |  | ||||||
| window.addEventListener('contextmenu', (e) => { // Not compatible with IE < 9
 |  | ||||||
| 
 |  | ||||||
|     if (e.target["nodeName"] === "INPUT") { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|     e.preventDefault(); |  | ||||||
|     return false; |  | ||||||
| }, false); |  | ||||||
|  |  | ||||||
|  | @ -124,7 +124,9 @@ | ||||||
|             "intro": "<h3>More thematic maps?</h3>Do you enjoy collecting geodata? <br/>There are more themes available.", |             "intro": "<h3>More thematic maps?</h3>Do you enjoy collecting geodata? <br/>There are more themes available.", | ||||||
|             "requestATheme": "If you want a custom-built theme, request it in the issue tracker", |             "requestATheme": "If you want a custom-built theme, request it in the issue tracker", | ||||||
|             "streetcomplete": "Another, similar application is <a href='https://play.google.com/store/apps/details?id=de.westnordost.streetcomplete' class='underline hover:text-blue-800' class='underline hover:text-blue-800' target='_blank'>StreetComplete</a>.", |             "streetcomplete": "Another, similar application is <a href='https://play.google.com/store/apps/details?id=de.westnordost.streetcomplete' class='underline hover:text-blue-800' class='underline hover:text-blue-800' target='_blank'>StreetComplete</a>.", | ||||||
|             "createYourOwnTheme": "Create your own MapComplete theme from scratch" |             "createYourOwnTheme": "Create your own MapComplete theme from scratch", | ||||||
|  |             "previouslyHiddenTitle": "Previously visited hidden themes", | ||||||
|  |             "hiddenExplanation": "These themes are only visible if you know the link..." | ||||||
|         }, |         }, | ||||||
|         "sharescreen": { |         "sharescreen": { | ||||||
|             "intro": "<h3>Share this map</h3> Share this map by copying the link below and sending it to friends and family:", |             "intro": "<h3>Share this map</h3> Share this map by copying the link below and sending it to friends and family:", | ||||||
|  |  | ||||||
|  | @ -1271,6 +1271,11 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "shortDescription": "Help to build an open dataset of UK addresses", |         "shortDescription": "Help to build an open dataset of UK addresses", | ||||||
|  |         "tileLayerSources": { | ||||||
|  |             "0": { | ||||||
|  |                 "name": "Property boundaries by osmuk.org" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "title": "UK Addresses" |         "title": "UK Addresses" | ||||||
|     }, |     }, | ||||||
|     "waste_basket": { |     "waste_basket": { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue