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 overpassMaxZoom: UIEventSource<number>; | ||||
|             readonly osmConnection: OsmConnection | ||||
|             readonly currentBounds: UIEventSource<BBox> | ||||
|             readonly currentBounds: UIEventSource<BBox>, | ||||
|         }) { | ||||
|         this.state = state; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,5 @@ | |||
| 
 | ||||
| import State from "../../../State"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import Loc from "../../../Models/Loc"; | ||||
| import TileHierarchy from "./TileHierarchy"; | ||||
|  | @ -25,7 +22,6 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | |||
|             leafletMap: any | ||||
|         } | ||||
|     ) { | ||||
|         state = State.state | ||||
|         const self = this; | ||||
| 
 | ||||
|         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 { | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | ||||
| import {LayerConfigJson} from "./LayerConfigJson"; | ||||
| import TilesourceConfig from "../TilesourceConfig"; | ||||
| 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. | ||||
|  */ | ||||
| export interface LayoutConfigJson { | ||||
| 
 | ||||
|     | ||||
|     /** | ||||
|      * 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 | ||||
|      */ | ||||
|     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. | ||||
|      * | ||||
|  | @ -269,6 +281,7 @@ export interface LayoutConfigJson { | |||
|     enableShowAllQuestions?: boolean; | ||||
|     enableDownload?: boolean; | ||||
|     enablePdfDownload?: boolean; | ||||
|     enableIframePopout?: true | boolean; | ||||
| 
 | ||||
|     /** | ||||
|      * 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 enableExportButton: boolean; | ||||
|     public readonly enablePdfDownload: boolean; | ||||
|     public readonly enableIframePopout: boolean; | ||||
| 
 | ||||
|     public readonly customCss?: string; | ||||
|     /* | ||||
|  | @ -54,8 +55,10 @@ export default class LayoutConfig { | |||
|     public readonly cacheTimeout?: number; | ||||
|     public readonly overpassUrl: string[]; | ||||
|     public readonly overpassTimeout: number; | ||||
|     public readonly overpassMaxZoom: number | ||||
|     public readonly osmApiTileSize: number | ||||
|     public readonly official: boolean; | ||||
| 
 | ||||
|   | ||||
|     constructor(json: LayoutConfigJson, official = true, context?: string) { | ||||
|         this.official = official; | ||||
|         this.id = json.id; | ||||
|  | @ -171,6 +174,7 @@ export default class LayoutConfig { | |||
|         this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; | ||||
|         this.enableExportButton = json.enableDownload ?? false; | ||||
|         this.enablePdfDownload = json.enablePdfDownload ?? false; | ||||
|         this.enableIframePopout = json.enableIframePopout ?? true | ||||
|         this.customCss = json.customCss; | ||||
|         this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) | ||||
|         this.overpassUrl = Constants.defaultOverpassUrls | ||||
|  | @ -182,6 +186,8 @@ export default class LayoutConfig { | |||
|             } | ||||
|         } | ||||
|         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 {BBox} from "./Logic/BBox"; | ||||
| import SelectedElementTagsUpdater from "./Logic/Actors/SelectedElementTagsUpdater"; | ||||
| import TilesourceConfig from "./Models/ThemeConfig/TilesourceConfig"; | ||||
| import FeaturePipelineState from "./Logic/State/FeaturePipelineState"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains the global state: a bunch of UI-event sources | ||||
|  */ | ||||
| 
 | ||||
| export default class State { | ||||
|     // The singleton of the global state
 | ||||
|     public static state: State; | ||||
| 
 | ||||
|     public readonly layoutToUse : LayoutConfig; | ||||
| 
 | ||||
|     /** | ||||
|      The mapping from id -> UIEventSource<properties> | ||||
| export default class State extends FeaturePipelineState { | ||||
|     /* The singleton of the global state | ||||
|      */ | ||||
|     public allElements: ElementStorage = new ElementStorage(); | ||||
|     /** | ||||
|      THe change handler | ||||
|      */ | ||||
|     public changes: Changes = new Changes(); | ||||
|     /** | ||||
|      The leaflet instance of the big basemap | ||||
|      */ | ||||
|     public leafletMap = new UIEventSource<L.Map>(undefined, "leafletmap"); | ||||
|     /** | ||||
|      * Background layer id | ||||
|      */ | ||||
|     public availableBackgroundLayers: UIEventSource<BaseLayer[]>; | ||||
|     /** | ||||
|      The user credentials | ||||
|      */ | ||||
|     public osmConnection: OsmConnection; | ||||
| 
 | ||||
|     public mangroveIdentity: MangroveIdentity; | ||||
| 
 | ||||
|     public favouriteLayers: UIEventSource<string[]>; | ||||
| 
 | ||||
|     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers"); | ||||
| 
 | ||||
|     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 | ||||
|     ); | ||||
|     public static state: FeaturePipelineState; | ||||
| 
 | ||||
|     constructor(layoutToUse: LayoutConfig) { | ||||
|         const self = this; | ||||
|         this.layoutToUse  = layoutToUse; | ||||
|         super(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 Svg from "../../Svg"; | ||||
| import Translations from "../i18n/Translations"; | ||||
|  | @ -8,31 +7,46 @@ import Constants from "../../Models/Constants"; | |||
| import Combine from "../Base/Combine"; | ||||
| import {TabbedComponent} from "../Base/TabbedComponent"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import UserDetails from "../../Logic/Osm/OsmConnection"; | ||||
| import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import UserRelatedState from "../../Logic/State/UserRelatedState"; | ||||
| 
 | ||||
| export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | ||||
| 
 | ||||
| 
 | ||||
|     constructor(isShown: UIEventSource<boolean>) { | ||||
|         const layoutToUse = State.state.layoutToUse; | ||||
|     constructor(isShown: UIEventSource<boolean>, | ||||
|                 currentTab: UIEventSource<number>, | ||||
|                 state: { | ||||
|                     layoutToUse: LayoutConfig, | ||||
|                     osmConnection: OsmConnection, | ||||
|                     featureSwitchShareScreen: UIEventSource<boolean>, | ||||
|                     featureSwitchMoreQuests: UIEventSource<boolean> | ||||
|                 } & UserRelatedState) { | ||||
|         const layoutToUse = state.layoutToUse; | ||||
|         super( | ||||
|             () => layoutToUse.title.Clone(), | ||||
|             () => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails, isShown), | ||||
|             "welcome", isShown | ||||
|             () => FullWelcomePaneWithTabs.GenerateContents(state, currentTab, 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); | ||||
|         | ||||
| 
 | ||||
|         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, | ||||
|                 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()}); | ||||
|         } | ||||
| 
 | ||||
|         if (State.state.featureSwitchMoreQuests.data) { | ||||
|         if (state.featureSwitchMoreQuests.data) { | ||||
| 
 | ||||
|             tabs.push({ | ||||
|                 header: Svg.add_img, | ||||
|                 content: new MoreScreen() | ||||
|                 content: new MoreScreen(state) | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         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 tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)] | ||||
|         const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown) | ||||
|         const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown)] | ||||
| 
 | ||||
|         const now = new Date() | ||||
|         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` | ||||
|          | ||||
| 
 | ||||
|         tabsWithAboutMc.push({ | ||||
|                 header: Svg.help, | ||||
|                 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")) | ||||
|         tabsWithAboutMc.forEach(c => c.content.SetClass("p-4")) | ||||
|          | ||||
| 
 | ||||
|         return new Toggle( | ||||
|             new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab), | ||||
|             new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab), | ||||
|             userDetails.map((userdetails: UserDetails) => | ||||
|             new TabbedComponent(tabsWithAboutMc, currentTab), | ||||
|             new TabbedComponent(tabs, currentTab), | ||||
|             state.osmConnection.userDetails.map((userdetails: UserDetails) => | ||||
|                 userdetails.loggedIn && | ||||
|                 userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock) | ||||
|         ) | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ import Combine from "../Base/Combine"; | |||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import AttributionPanel from "./AttributionPanel"; | ||||
| import State from "../../State"; | ||||
| import ContributorCount from "../../Logic/ContributorCount"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import MapControlButton from "../MapControlButton"; | ||||
|  | @ -13,16 +12,33 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | |||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| 
 | ||||
| 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( | ||||
|             () => Translations.t.general.attribution.attributionTitle.Clone(), | ||||
|             () => | ||||
|                 new AttributionPanel( | ||||
|                     State.state.layoutToUse, | ||||
|                     state.layoutToUse, | ||||
|                     new ContributorCount(state).Contributors | ||||
|                 ), | ||||
|             undefined | ||||
|  | @ -38,50 +54,50 @@ export default class LeftControls extends Combine { | |||
| 
 | ||||
|         const toggledDownload = new Toggle( | ||||
|             new AllDownloads( | ||||
|                 State.state.downloadControlIsOpened | ||||
|                 guiState.downloadControlIsOpened | ||||
|             ).SetClass("block p-1 rounded-full"), | ||||
|             new MapControlButton(Svg.download_svg()) | ||||
|                 .onClick(() => State.state.downloadControlIsOpened.setData(true)), | ||||
|             State.state.downloadControlIsOpened | ||||
|                 .onClick(() => guiState.downloadControlIsOpened.setData(true)), | ||||
|             guiState.downloadControlIsOpened | ||||
|         ) | ||||
| 
 | ||||
|         const downloadButtonn = new Toggle( | ||||
|             toggledDownload, | ||||
|             undefined, | ||||
|             State.state.featureSwitchEnableExport.map(downloadEnabled => downloadEnabled || State.state.featureSwitchExportAsPdf.data, | ||||
|                 [State.state.featureSwitchExportAsPdf]) | ||||
|             state.featureSwitchEnableExport.map(downloadEnabled => downloadEnabled || state.featureSwitchExportAsPdf.data, | ||||
|                 [state.featureSwitchExportAsPdf]) | ||||
|         ); | ||||
| 
 | ||||
|         const toggledFilter = new Toggle( | ||||
|             new ScrollableFullScreen( | ||||
|                 () => 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" | ||||
|                     ), | ||||
|                 undefined, | ||||
|                 State.state.filterIsOpened | ||||
|                 guiState.filterViewIsOpened | ||||
|             ), | ||||
|             new MapControlButton(Svg.filter_svg()) | ||||
|                 .onClick(() => State.state.filterIsOpened.setData(true)), | ||||
|             State.state.filterIsOpened | ||||
|                 .onClick(() => guiState.filterViewIsOpened.setData(true)), | ||||
|             guiState.filterViewIsOpened | ||||
|         ) | ||||
| 
 | ||||
|         const filterButton = new Toggle( | ||||
|             toggledFilter, | ||||
|             undefined, | ||||
|             State.state.featureSwitchFilter | ||||
|             state.featureSwitchFilter | ||||
|         ); | ||||
| 
 | ||||
| 
 | ||||
|         State.state.locationControl.addCallback(() => { | ||||
|         state.locationControl.addCallback(() => { | ||||
|             // Close the layer selection when the map is moved
 | ||||
|             toggledDownload.isEnabled.setData(false); | ||||
|             copyrightButton.isEnabled.setData(false); | ||||
|             toggledFilter.isEnabled.setData(false); | ||||
|         }); | ||||
| 
 | ||||
|         State.state.selectedElement.addCallbackAndRunD((_) => { | ||||
|         state.selectedElement.addCallbackAndRunD((_) => { | ||||
|             toggledDownload.isEnabled.setData(false); | ||||
|             copyrightButton.isEnabled.setData(false); | ||||
|             toggledFilter.isEnabled.setData(false); | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; | ||||
| import Svg from "../../Svg"; | ||||
| import State from "../../State"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import Translations from "../i18n/Translations"; | ||||
|  | @ -11,15 +10,21 @@ import LanguagePicker from "../LanguagePicker"; | |||
| import IndexText from "./IndexText"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| 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 { | ||||
| 
 | ||||
| 
 | ||||
|     constructor(onMainScreen: boolean = false) { | ||||
|         super(MoreScreen.Init(onMainScreen, State.state)); | ||||
|     } | ||||
| 
 | ||||
|     private static Init(onMainScreen: boolean, state: State): BaseUIElement [] { | ||||
|     constructor(state: UserRelatedState & { | ||||
|         locationControl?: UIEventSource<Loc>, | ||||
|         layoutToUse?: LayoutConfig | ||||
|     }, onMainScreen: boolean = false) { | ||||
|         const tr = Translations.t.general.morescreen; | ||||
|         let intro: BaseUIElement = tr.intro.Clone(); | ||||
|         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" | ||||
|         } | ||||
| 
 | ||||
|         return [ | ||||
|         super([ | ||||
|             intro, | ||||
|             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") | ||||
|         ]; | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     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[] = [] | ||||
|             if (customThemes.length > 0) { | ||||
|                 els.push(Translations.t.general.customThemeIntro.Clone()) | ||||
| 
 | ||||
|                 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) | ||||
|             } | ||||
|             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 buttons = officialThemes.map((layout) => { | ||||
|  | @ -66,10 +100,10 @@ export default class MoreScreen extends Combine { | |||
|                 console.trace("Layout is undefined") | ||||
|                 return undefined | ||||
|             } | ||||
|             const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass); | ||||
|             const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass); | ||||
|             if (layout.id === personal.id) { | ||||
|                 return new VariableUiElement( | ||||
|                     State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount) | ||||
|                     state.osmConnection.userDetails.map(userdetails => userdetails.csCount) | ||||
|                         .map(csCount => { | ||||
|                             if (csCount < Constants.userJourney.personalLayoutUnlock) { | ||||
|                                 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 | ||||
|     * */ | ||||
|     private static createCustomGeneratorButton(state: State): VariableUiElement { | ||||
|     private static createCustomGeneratorButton(state: { osmConnection: OsmConnection }): VariableUiElement { | ||||
|         const tr = Translations.t.general.morescreen; | ||||
|         return new VariableUiElement( | ||||
|             state.osmConnection.userDetails.map(userDetails => { | ||||
|  | @ -111,13 +145,22 @@ export default class MoreScreen extends Combine { | |||
| 
 | ||||
|     /** | ||||
|      * Creates a button linking to the given theme | ||||
|      * @param layout | ||||
|      * @param customThemeDefinition | ||||
|      * @private | ||||
|      */ | ||||
|     private static createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined): BaseUIElement { | ||||
|         if (layout === undefined) { | ||||
|             return undefined; | ||||
|     private static createLinkButton( | ||||
|         state: { | ||||
|             locationControl?: UIEventSource<Loc>, | ||||
|             layoutToUse?: LayoutConfig | ||||
|         }, layout: LayoutConfig, customThemeDefinition: string = undefined | ||||
|     ): | ||||
|         BaseUIElement { | ||||
|         if (layout | ||||
| 
 | ||||
|             === | ||||
|             undefined | ||||
|         ) { | ||||
|             return | ||||
|             undefined; | ||||
|         } | ||||
|         if (layout.id === undefined) { | ||||
|             console.error("ID is undefined for layout", layout); | ||||
|  | @ -126,11 +169,11 @@ export default class MoreScreen extends Combine { | |||
|         if (layout.hideFromOverview) { | ||||
|             return undefined; | ||||
|         } | ||||
|         if (layout.id === State.state.layoutToUse?.id) { | ||||
|         if (layout.id === state?.layoutToUse?.id) { | ||||
|             return undefined; | ||||
|         } | ||||
| 
 | ||||
|         const currentLocation = State.state.locationControl; | ||||
|         const currentLocation = state?.locationControl; | ||||
| 
 | ||||
|         let path = window.location.pathname; | ||||
|         // 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}` | ||||
|         } | ||||
| 
 | ||||
|         const linkText = currentLocation.map(currentLocation => { | ||||
|         const linkText = currentLocation?.map(currentLocation => { | ||||
|             const params = [ | ||||
|                 ["z", currentLocation?.zoom], | ||||
|                 ["lat", currentLocation?.lat], | ||||
|  | @ -160,7 +203,7 @@ export default class MoreScreen extends Combine { | |||
|                 .map(part => part[0] + "=" + part[1]) | ||||
|                 .join("&") | ||||
|             return `${linkPrefix}${params}${linkSuffix}`; | ||||
|         }) | ||||
|         }) ?? new UIEventSource<string>(`${linkPrefix}${linkSuffix}`) | ||||
| 
 | ||||
| 
 | ||||
|         let description = Translations.WT(layout.shortDescription).Clone(); | ||||
|  |  | |||
|  | @ -2,38 +2,38 @@ import Combine from "../Base/Combine"; | |||
| import Toggle from "../Input/Toggle"; | ||||
| import MapControlButton from "../MapControlButton"; | ||||
| import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"; | ||||
| import State from "../../State"; | ||||
| import Svg from "../../Svg"; | ||||
| import MapState from "../../Logic/State/MapState"; | ||||
| 
 | ||||
| export default class RightControls extends Combine { | ||||
| 
 | ||||
|     constructor() { | ||||
|     constructor(state:MapState) { | ||||
|         const geolocationButton = new Toggle( | ||||
|             new MapControlButton( | ||||
|                 new GeoLocationHandler( | ||||
|                     State.state.currentGPSLocation, | ||||
|                     State.state.leafletMap, | ||||
|                     State.state.layoutToUse | ||||
|                     state.currentGPSLocation, | ||||
|                     state.leafletMap, | ||||
|                     state.layoutToUse | ||||
|                 ), { | ||||
|                     dontStyle: true | ||||
|                 } | ||||
|             ), | ||||
|             undefined, | ||||
|             State.state.featureSwitchGeolocation | ||||
|             state.featureSwitchGeolocation | ||||
|         ); | ||||
| 
 | ||||
|         const plus = new MapControlButton( | ||||
|             Svg.plus_svg() | ||||
|         ).onClick(() => { | ||||
|             State.state.locationControl.data.zoom++; | ||||
|             State.state.locationControl.ping(); | ||||
|             state.locationControl.data.zoom++; | ||||
|             state.locationControl.ping(); | ||||
|         }); | ||||
| 
 | ||||
|         const min = new MapControlButton( | ||||
|             Svg.min_svg() | ||||
|         ).onClick(() => { | ||||
|             State.state.locationControl.data.zoom--; | ||||
|             State.state.locationControl.ping(); | ||||
|             state.locationControl.data.zoom--; | ||||
|             state.locationControl.ping(); | ||||
|         }); | ||||
| 
 | ||||
|         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 {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import Svg from "../../Svg"; | ||||
| import State from "../../State"; | ||||
| import {TextField} from "../Input/TextField"; | ||||
| import {Geocoding} from "../../Logic/Osm/Geocoding"; | ||||
| import Translations from "../i18n/Translations"; | ||||
|  | @ -10,7 +9,10 @@ import Hash from "../../Logic/Web/Hash"; | |||
| import Combine from "../Base/Combine"; | ||||
| 
 | ||||
| export default class SearchAndGo extends Combine { | ||||
|     constructor() { | ||||
|     constructor(state: { | ||||
|         leafletMap: UIEventSource<any>, | ||||
|         selectedElement: UIEventSource<any> | ||||
|     }) { | ||||
|         const goButton = Svg.search_ui().SetClass( | ||||
|             "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[1], bb[3]], | ||||
|                     ]; | ||||
|                     State.state.selectedElement.setData(undefined); | ||||
|                     state.selectedElement.setData(undefined); | ||||
|                     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); | ||||
|                 }, | ||||
|                 () => { | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Svg from "../../Svg"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import State from "../../State"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import Constants from "../../Models/Constants"; | ||||
|  | @ -12,7 +11,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils"; | |||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import UserDetails from "../../Logic/Osm/OsmConnection"; | ||||
| import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||
| import LocationInput from "../Input/LocationInput"; | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||
| 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 FilteredLayer from "../../Models/FilteredLayer"; | ||||
| 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: | ||||
|  | @ -38,9 +42,22 @@ interface PresetInfo extends PresetConfig { | |||
| 
 | ||||
| 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()) | ||||
|             .onClick(() => State.state.osmConnection.AttemptLogin()); | ||||
|             .onClick(() => state.osmConnection.AttemptLogin()); | ||||
|         const readYourMessages = new Combine([ | ||||
|             Translations.t.general.readYourMessages.Clone().SetClass("alert"), | ||||
|             new SubtleButton(Svg.envelope_ui(), | ||||
|  | @ -50,20 +67,21 @@ export default class SimpleAddUI extends Toggle { | |||
| 
 | ||||
|         const selectedPreset = new UIEventSource<PresetInfo>(undefined); | ||||
|         isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
 | ||||
|         State.state.LastClickLocation.addCallback( _ => selectedPreset.setData(undefined)) | ||||
|          | ||||
|         const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset) | ||||
|         state.LastClickLocation.addCallback(_ => selectedPreset.setData(undefined)) | ||||
| 
 | ||||
|         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, { | ||||
|                 theme: State.state?.layoutToUse?.id ?? "unkown", | ||||
|                 theme: state.layoutToUse?.id ?? "unkown", | ||||
|                 changeType: "create", | ||||
|                 snapOnto: snapOntoWay}) | ||||
|             await State.state.changes.applyAction(newElementAction) | ||||
|                 snapOnto: snapOntoWay | ||||
|             }) | ||||
|             await state.changes.applyAction(newElementAction) | ||||
|             selectedPreset.setData(undefined) | ||||
|             isShown.setData(false) | ||||
|             State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get( | ||||
|             state.selectedElement.setData(state.allElements.ContainingFeatures.get( | ||||
|                 newElementAction.newElementId | ||||
|             )) | ||||
|         } | ||||
|  | @ -73,7 +91,7 @@ export default class SimpleAddUI extends Toggle { | |||
|                     if (preset === undefined) { | ||||
|                         return presetsOverview | ||||
|                     } | ||||
|                     return SimpleAddUI.CreateConfirmButton(preset, | ||||
|                     return SimpleAddUI.CreateConfirmButton(state, filterViewIsOpened, preset, | ||||
|                         (tags, location, snapOntoWayId?: string) => { | ||||
|                             if (snapOntoWayId === undefined) { | ||||
|                                 createNewPoint(tags, location, undefined) | ||||
|  | @ -97,18 +115,18 @@ export default class SimpleAddUI extends Toggle { | |||
|                     new Toggle( | ||||
|                         addUi, | ||||
|                         Translations.t.general.add.stillLoading.Clone().SetClass("alert"), | ||||
|                         State.state.featurePipeline.somethingLoaded | ||||
|                         state.featurePipeline.somethingLoaded | ||||
|                     ), | ||||
|                     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, | ||||
|                 State.state.osmConnection.userDetails.map((userdetails: UserDetails) => | ||||
|                 state.osmConnection.userDetails.map((userdetails: UserDetails) => | ||||
|                     userdetails.csCount >= Constants.userJourney.addNewPointWithUnreadMessagesUnlock || | ||||
|                     userdetails.unreadMessages == 0) | ||||
|             ), | ||||
|             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, | ||||
|                                        cancel: () => void): BaseUIElement { | ||||
| 
 | ||||
|         let location = State.state.LastClickLocation; | ||||
|         let location = state.LastClickLocation; | ||||
|         let preciseInput: LocationInput = undefined | ||||
|         if (preset.preciseInput !== undefined) { | ||||
|             // We uncouple the event source
 | ||||
|  | @ -143,7 +168,6 @@ export default class SimpleAddUI extends Toggle { | |||
|             } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|             const tags = TagUtils.KVtoProperties(preset.tags ?? []); | ||||
|             preciseInput = new LocationInput({ | ||||
|                 mapBackground: backgroundLayer, | ||||
|  | @ -160,24 +184,24 @@ export default class SimpleAddUI extends Toggle { | |||
|             if (preset.preciseInput.snapToLayers) { | ||||
|                 // We have to snap to certain layers.
 | ||||
|                 // Lets fetch them
 | ||||
|                  | ||||
|                 let loadedBbox : BBox= undefined | ||||
| 
 | ||||
|                 let loadedBbox: BBox = undefined | ||||
|                 mapBounds?.addCallbackAndRunD(bbox => { | ||||
|                     if(loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)){ | ||||
|                     if (loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)) { | ||||
|                         // All is already there
 | ||||
|                         // return;
 | ||||
|                     } | ||||
| 
 | ||||
|                     bbox = bbox.pad(2); | ||||
|                     loadedBbox = bbox; | ||||
|                     const allFeatures: {feature: any}[] = [] | ||||
|                     const allFeatures: { feature: any }[] = [] | ||||
|                     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) | ||||
|                 }) | ||||
|             } | ||||
|              | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -205,7 +229,7 @@ export default class SimpleAddUI extends Toggle { | |||
|                     Translations.t.general.add.openLayerControl | ||||
|                 ]) | ||||
|             ) | ||||
|                 .onClick(() => State.state.filterIsOpened.setData(true)) | ||||
|                 .onClick(() => filterViewIsOpened.setData(true)) | ||||
| 
 | ||||
| 
 | ||||
|         const openLayerOrConfirm = new Toggle( | ||||
|  | @ -234,36 +258,35 @@ export default class SimpleAddUI extends Toggle { | |||
|             openLayerOrConfirm, | ||||
|             disableFilter, | ||||
|             preset.layerToAddTo.appliedFilters.map(filters => { | ||||
|                 if(filters === undefined || filters.length === 0){ | ||||
|                 if (filters === undefined || filters.length === 0) { | ||||
|                     return true; | ||||
|                 } | ||||
|                 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; | ||||
|                     } | ||||
|                     if(filter.selected !== undefined){ | ||||
|                     if (filter.selected !== undefined) { | ||||
|                         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
 | ||||
|                             return false; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
| 
 | ||||
|             }) | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|         const tagInfo = SimpleAddUI.CreateTagInfoFor(preset); | ||||
|         const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection); | ||||
| 
 | ||||
|         const cancelButton = new SubtleButton(Svg.close_ui(), | ||||
|             Translations.t.general.cancel | ||||
|         ).onClick(cancel) | ||||
| 
 | ||||
|         return new Combine([ | ||||
|             // Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
 | ||||
|             State.state.osmConnection.userDetails.data.dryRun ? | ||||
|             state.osmConnection.userDetails.data.dryRun ? | ||||
|                 Translations.t.general.testing.Clone().SetClass("alert") : undefined, | ||||
|             disableFiltersOrConfirm, | ||||
|             cancelButton, | ||||
|  | @ -274,24 +297,29 @@ export default class SimpleAddUI extends Toggle { | |||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private static CreateTagInfoFor(preset: PresetInfo, optionallyLinkToWiki = true) { | ||||
|         const csCount = State.state.osmConnection.userDetails.data.csCount; | ||||
|     private static CreateTagInfoFor(preset: PresetInfo, osmConnection: OsmConnection, optionallyLinkToWiki = true) { | ||||
|         const csCount = osmConnection.userDetails.data.csCount; | ||||
|         return new Toggle( | ||||
|             Translations.t.general.add.presetInfo.Subs({ | ||||
|                 tags: preset.tags.map(t => t.asHumanString(optionallyLinkToWiki && csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"), | ||||
|             }).SetStyle("word-break: break-all"), | ||||
| 
 | ||||
|             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 { | ||||
|         const presetButtons = SimpleAddUI.CreatePresetButtons(selectedPreset) | ||||
|     private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>, | ||||
|                                          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 testMode: BaseUIElement = undefined; | ||||
|         if (State.state.osmConnection?.userDetails?.data?.dryRun) { | ||||
|         if (state.osmConnection?.userDetails?.data?.dryRun) { | ||||
|             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( | ||||
|             preset.icon(), | ||||
|             new Combine([ | ||||
|  | @ -316,11 +344,17 @@ export default class SimpleAddUI extends Toggle { | |||
| 
 | ||||
|     /* | ||||
|     * 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 = []; | ||||
|         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
 | ||||
|                 continue; | ||||
|             } | ||||
|  | @ -346,7 +380,7 @@ export default class SimpleAddUI extends Toggle { | |||
|                     preciseInput: preset.preciseInput | ||||
|                 } | ||||
| 
 | ||||
|                 const button = SimpleAddUI.CreatePresetSelectButton(presetInfo); | ||||
|                 const button = SimpleAddUI.CreatePresetSelectButton(presetInfo, state.osmConnection); | ||||
|                 button.onClick(() => { | ||||
|                     selectedPreset.setData(presetInfo) | ||||
|                 }) | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import Svg from "../../Svg"; | ||||
| import State from "../../State"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import LanguagePicker from "../LanguagePicker"; | ||||
|  | @ -8,24 +7,25 @@ import Translations from "../i18n/Translations"; | |||
| import Link from "../Base/Link"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import Img from "../Base/Img"; | ||||
| import MapState from "../../Logic/State/MapState"; | ||||
| 
 | ||||
| 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 | ||||
|             .Clone() | ||||
|             .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 = | ||||
|             Svg.logout_svg() | ||||
|                 .onClick(() => { | ||||
|                     State.state.osmConnection.LogOut(); | ||||
|                     state.osmConnection.LogOut(); | ||||
|                 }); | ||||
| 
 | ||||
| 
 | ||||
|  | @ -39,15 +39,15 @@ export default class UserBadge extends Toggle { | |||
|                         return " "; | ||||
|                     }) | ||||
|                 ).onClick(() => { | ||||
|                     const home = State.state.osmConnection.userDetails.data?.home; | ||||
|                     const home = state.osmConnection.userDetails.data?.home; | ||||
|                     if (home === undefined) { | ||||
|                         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 languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.language) ?? new FixedUiElement("")) | ||||
|                 const languagePicker = (LanguagePicker.CreateLanguagePicker(state.layoutToUse.language) ?? new FixedUiElement("")) | ||||
|                     .SetStyle("width:min-content;"); | ||||
| 
 | ||||
|                 let messageSpan = | ||||
|  | @ -129,7 +129,7 @@ export default class UserBadge extends Toggle { | |||
|         super( | ||||
|             userBadge, | ||||
|             loginButton, | ||||
|             State.state.osmConnection.isLoggedIn | ||||
|             state.osmConnection.isLoggedIn | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,12 +1,11 @@ | |||
| import Translations from "./i18n/Translations"; | ||||
| import State from "../State"; | ||||
| import {VariableUiElement} from "./Base/VariableUIElement"; | ||||
| import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; | ||||
| 
 | ||||
| export default class CenterMessageBox extends VariableUiElement { | ||||
| 
 | ||||
|     constructor() { | ||||
|         const state = State.state; | ||||
|         const updater = State.state.featurePipeline; | ||||
|     constructor(state: FeaturePipelineState) { | ||||
|         const updater = state.featurePipeline; | ||||
|         const t = Translations.t.centerMessage; | ||||
|         const message = updater.runningQuery.map( | ||||
|             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() | ||||
|         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) | ||||
|         } | ||||
| 
 | ||||
|         State.state.AddAllOverlaysToMap(minimap.leafletMap) | ||||
|     } | ||||
| 
 | ||||
|     private cleanup() { | ||||
|  |  | |||
|  | @ -176,7 +176,8 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO | |||
|                         enablePopups: false, | ||||
|                         zoomToFeatures: false, | ||||
|                         leafletMap: this.map.leafletMap, | ||||
|                         layers: State.state.filteredLayers | ||||
|                         layers: State.state.filteredLayers, | ||||
|                     allElements: State.state.allElements | ||||
|                     } | ||||
|                 ) | ||||
|                 // Show the central point
 | ||||
|  | @ -191,7 +192,9 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO | |||
|                         enablePopups: false, | ||||
|                         zoomToFeatures: false, | ||||
|                         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 BaseUIElement from "../BaseUIElement"; | ||||
| 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. | ||||
|  | @ -24,4 +25,16 @@ export default class Toggle extends VariableUiElement { | |||
|         }) | ||||
|         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, | ||||
|             zoomToFeatures: false, | ||||
|             enablePopups: false, | ||||
|             layerToShow: SplitRoadWizard.splitLayerStyling | ||||
|             layerToShow: SplitRoadWizard.splitLayerStyling, | ||||
|         }) | ||||
| 
 | ||||
|         new ShowDataMultiLayer({ | ||||
|  | @ -76,7 +76,8 @@ export default class SplitRoadWizard extends Toggle { | |||
|             layers: State.state.filteredLayers, | ||||
|             leafletMap: miniMap.leafletMap, | ||||
|             enablePopups: false, | ||||
|             zoomToFeatures: true | ||||
|             zoomToFeatures: true, | ||||
|             allElements: State.state.allElements, | ||||
|         }) | ||||
| 
 | ||||
|         /** | ||||
|  |  | |||
|  | @ -4,9 +4,9 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import FeatureInfoBox from "../Popup/FeatureInfoBox"; | ||||
| import State from "../../State"; | ||||
| import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import {ElementStorage} from "../../Logic/ElementStorage"; | ||||
| import Hash from "../../Logic/Web/Hash"; | ||||
| 
 | ||||
| export default class ShowDataLayer { | ||||
| 
 | ||||
|  | @ -14,7 +14,8 @@ export default class ShowDataLayer { | |||
|     private readonly _enablePopups: boolean; | ||||
|     private readonly _features: UIEventSource<{ feature: any }[]> | ||||
|     private readonly _layerToShow: LayerConfig; | ||||
| 
 | ||||
|     private readonly _selectedElement: UIEventSource<any> | ||||
|     private readonly allElements : ElementStorage | ||||
|     // Used to generate a fresh ID when needed
 | ||||
|     private _cleanCount = 0; | ||||
|     private geoLayer = undefined; | ||||
|  | @ -43,6 +44,8 @@ export default class ShowDataLayer { | |||
|         const features = options.features.features.map(featFreshes => featFreshes.map(ff => ff.feature)); | ||||
|         this._features = features; | ||||
|         this._layerToShow = options.layerToShow; | ||||
|         this._selectedElement = options.selectedElement | ||||
|         this.allElements = options.allElements; | ||||
|         const self = this; | ||||
| 
 | ||||
|         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) { | ||||
|                 return; | ||||
|             } | ||||
|  | @ -162,7 +165,7 @@ export default class ShowDataLayer { | |||
| 
 | ||||
| 
 | ||||
|     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
 | ||||
|         const layer = this._layerToShow | ||||
|         return layer?.GenerateLeafletStyle(tagsSource, true); | ||||
|  | @ -178,10 +181,7 @@ export default class ShowDataLayer { | |||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let tagSource = State.state.allElements.getEventSourceById(feature.properties.id) | ||||
|         if (tagSource === undefined) { | ||||
|             tagSource = new UIEventSource<any>(feature.properties) | ||||
|         } | ||||
|         let tagSource = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource<any>(feature.properties) | ||||
|         const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0) | ||||
|         const style = layer.GenerateLeafletStyle(tagSource, clickable); | ||||
|         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>`) | ||||
|         leafletLayer.on("popupopen", () => { | ||||
|             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.isShown.addCallback(isShown => { | ||||
|                     if (!isShown) { | ||||
|                         State.state.selectedElement.setData(undefined); | ||||
|                         this._selectedElement?.setData(undefined); | ||||
|                         leafletLayer.closePopup() | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|             infobox.AttachTo(id) | ||||
|             infobox.Activate(); | ||||
|             if (State.state?.selectedElement?.data?.properties?.id !== feature.properties.id) { | ||||
|                 State.state.selectedElement.setData(feature) | ||||
|             if (this._selectedElement?.data?.properties?.id !== feature.properties.id) { | ||||
|                 this._selectedElement?.setData(feature) | ||||
|             } | ||||
| 
 | ||||
|         }); | ||||
|  | @ -254,6 +254,7 @@ export default class ShowDataLayer { | |||
|             feature: feature, | ||||
|             leafletlayer: leafletLayer | ||||
|         }) | ||||
|          | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,11 @@ | |||
| import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {ElementStorage} from "../../Logic/ElementStorage"; | ||||
| 
 | ||||
| export interface ShowDataLayerOptions { | ||||
|     features: FeatureSource, | ||||
|     selectedElement?: UIEventSource<any>, | ||||
|     allElements?: ElementStorage, | ||||
|     leafletMap: UIEventSource<L.Map>, | ||||
|     enablePopups?: true | boolean, | ||||
|     zoomToFeatures?: false | boolean, | ||||
|  |  | |||
|  | @ -208,7 +208,8 @@ export default class SpecialVisualizations { | |||
|                             enablePopups: false, | ||||
|                             zoomToFeatures: true, | ||||
|                             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) | ||||
|     } | ||||
|      | ||||
|     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, | ||||
|   "startZoom": 16, | ||||
|   "widenFactor": 1.2, | ||||
|   "overpassMaxZoom": 0, | ||||
|   "layers": [], | ||||
|   "roamingRenderings": [] | ||||
| } | ||||
							
								
								
									
										176
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										176
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -1,27 +1,24 @@ | |||
| import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; | ||||
| import {FixedUiElement} from "./UI/Base/FixedUiElement"; | ||||
| import {InitUiElements} from "./InitUiElements"; | ||||
| 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 Translations from "./UI/i18n/Translations"; | ||||
| import ValidatedTextField from "./UI/Input/ValidatedTextField"; | ||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | ||||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; | ||||
| import Constants from "./Models/Constants"; | ||||
| import MinimapImplementation from "./UI/Base/MinimapImplementation"; | ||||
| import CountryCoder from "latlon2country/index"; | ||||
| 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() | ||||
| // 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) | ||||
| SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | ||||
| Utils.DisableLongPresses() | ||||
| 
 | ||||
| let defaultLayout = "" | ||||
| // --------------------- Special actions based on the parameters -----------------
 | ||||
| // @ts-ignore
 | ||||
| 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) { | ||||
|     defaultLayout = "buurtnatuur" | ||||
| } | ||||
| class Init { | ||||
| 
 | ||||
| 
 | ||||
| let testing: UIEventSource<string>; | ||||
| if (QueryParameters.GetQueryParameter("backend", undefined).data !== "osm-test" && | ||||
|     (location.hostname === "localhost" || location.hostname === "127.0.0.1")) { | ||||
|     testing = QueryParameters.GetQueryParameter("test", "true"); | ||||
|     // Set to true if testing and changes should NOT be saved
 | ||||
|     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
 | ||||
|     // Overpass.testUrl = "http://127.0.0.1:8080/streetwidths.geojson";
 | ||||
| } else { | ||||
|     testing = QueryParameters.GetQueryParameter("test", "false"); | ||||
| } | ||||
|     public static Init(layoutToUse: LayoutConfig, encoded: string) { | ||||
| 
 | ||||
|         if(layoutToUse === null){ | ||||
|             // Something went wrong, error message is already on screen
 | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         if (layoutToUse === undefined) { | ||||
|             // No layout found
 | ||||
|             new AllThemesGui() | ||||
|             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 FixedUiElement("<a>If this message persist, something went wrong - click here to try again</a>") | ||||
|         .SetClass("link-underline small") | ||||
|  | @ -93,71 +95,13 @@ new Combine(["Initializing... <br/>", | |||
| 
 | ||||
|         })]) | ||||
|     .AttachTo("centermessage"); // Add an initialization and reset button if something goes wrong
 | ||||
| document.getElementById("decoration-desktop").remove(); | ||||
| 
 | ||||
| 
 | ||||
| if (layoutFromBase64.startsWith("http")) { | ||||
|     const link = layoutFromBase64; | ||||
|     console.log("Downloading map theme from ", link); | ||||
|     new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`) | ||||
|         .AttachTo("centermessage"); | ||||
| DetermineLayout.GetLayout().then(value => { | ||||
|     console.log("Got ", value) | ||||
|     Init.Init(value[0], value[1]) | ||||
| }).catch(err => { | ||||
|     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.", | ||||
|             "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>.", | ||||
|             "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": { | ||||
|             "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", | ||||
|         "tileLayerSources": { | ||||
|             "0": { | ||||
|                 "name": "Property boundaries by osmuk.org" | ||||
|             } | ||||
|         }, | ||||
|         "title": "UK Addresses" | ||||
|     }, | ||||
|     "waste_basket": { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue