From b8abbc95059f826f115ee9d4bb46cc24ea553766 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 15 Oct 2021 14:52:11 +0200 Subject: [PATCH] More refactoring to fix the tests --- Logic/Actors/AvailableBaseLayers.ts | 302 ++---------------- .../AvailableBaseLayersImplementation.ts | 292 +++++++++++++++++ Logic/Actors/OverpassFeatureSource.ts | 2 +- Logic/Actors/StrayClickHandler.ts | 1 + Logic/State/FeaturePipelineState.ts | 45 +-- Logic/State/MapState.ts | 55 +--- Logic/State/UserRelatedState.ts | 39 +++ Logic/UIEventSource.ts | 2 +- Models/ThemeConfig/Json/LayoutConfigJson.ts | 2 +- UI/DefaultGUI.ts | 62 +++- UI/Input/ValidatedTextField.ts | 7 +- UI/Popup/MoveWizard.ts | 3 +- UI/ShowDataLayer/ShowOverlayLayer.ts | 42 +-- .../ShowOverlayLayerImplementation.ts | 45 +++ .../layers/home_location/home_location.json | 17 + index.ts | 9 +- 16 files changed, 507 insertions(+), 418 deletions(-) create mode 100644 Logic/Actors/AvailableBaseLayersImplementation.ts create mode 100644 UI/ShowDataLayer/ShowOverlayLayerImplementation.ts create mode 100644 assets/layers/home_location/home_location.json diff --git a/Logic/Actors/AvailableBaseLayers.ts b/Logic/Actors/AvailableBaseLayers.ts index e84ecc6356..ce77e46d7d 100644 --- a/Logic/Actors/AvailableBaseLayers.ts +++ b/Logic/Actors/AvailableBaseLayers.ts @@ -1,298 +1,40 @@ -import * as editorlayerindex from "../../assets/editor-layer-index.json" import BaseLayer from "../../Models/BaseLayer"; -import * as L from "leaflet"; -import {TileLayer} from "leaflet"; -import * as X from "leaflet-providers"; import {UIEventSource} from "../UIEventSource"; -import {GeoOperations} from "../GeoOperations"; -import {Utils} from "../../Utils"; import Loc from "../../Models/Loc"; +export interface AvailableBaseLayersObj { + readonly osmCarto: BaseLayer; + layerOverview: BaseLayer[]; + AvailableLayersAt(location: UIEventSource): UIEventSource + SelectBestLayerAccordingTo(location: UIEventSource, preferedCategory: UIEventSource): UIEventSource ; + +} + /** * Calculates which layers are available at the current location * Changes the basemap */ export default class AvailableBaseLayers { + + + public static layerOverview: BaseLayer[]; + public static osmCarto: BaseLayer; - - public static osmCarto: BaseLayer = - { - id: "osm", - name: "OpenStreetMap", - layer: () => AvailableBaseLayers.CreateBackgroundLayer("osm", "OpenStreetMap", - "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright", - 19, - false, false), - feature: null, - max_zoom: 19, - min_zoom: 0, - isBest: false, // This is a lie! Of course OSM is the best map! (But not in this context) - category: "osmbasedmap" - } - - public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); - - public static AvailableLayersAt(location: UIEventSource): UIEventSource { - const source = location.map( - (currentLocation) => { - - if (currentLocation === undefined) { - return AvailableBaseLayers.layerOverview; - } - - const currentLayers = source?.data; // A bit unorthodox - I know - const newLayers = AvailableBaseLayers.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat); - - if (currentLayers === undefined) { - return newLayers; - } - if (newLayers.length !== currentLayers.length) { - return newLayers; - } - for (let i = 0; i < newLayers.length; i++) { - if (newLayers[i].name !== currentLayers[i].name) { - return newLayers; - } - } - - return currentLayers; - }); - return source; + private static implementation: AvailableBaseLayersObj + + static AvailableLayersAt(location: UIEventSource): UIEventSource { + return AvailableBaseLayers.implementation.AvailableLayersAt(location); } - public static SelectBestLayerAccordingTo(location: UIEventSource, preferedCategory: UIEventSource): UIEventSource { - return AvailableBaseLayers.AvailableLayersAt(location).map(available => { - // First float all 'best layers' to the top - available.sort((a, b) => { - if (a.isBest && b.isBest) { - return 0; - } - if (!a.isBest) { - return 1 - } - - return -1; - } - ) - - if (preferedCategory.data === undefined) { - return available[0] - } - - let prefered: string [] - if (typeof preferedCategory.data === "string") { - prefered = [preferedCategory.data] - } else { - prefered = preferedCategory.data; - } - - prefered.reverse(); - for (const category of prefered) { - //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top - available.sort((a, b) => { - if (a.category === category && b.category === category) { - return 0; - } - if (a.category !== category) { - return 1 - } - - return -1; - } - ) - } - return available[0] - }) - } - - private static CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] { - const availableLayers = [AvailableBaseLayers.osmCarto] - const globalLayers = []; - for (const layerOverviewItem of AvailableBaseLayers.layerOverview) { - const layer = layerOverviewItem; - - if (layer.feature?.geometry === undefined || layer.feature?.geometry === null) { - globalLayers.push(layer); - continue; - } - - if (lon === undefined || lat === undefined) { - continue; - } - - if (GeoOperations.inside([lon, lat], layer.feature)) { - availableLayers.push(layer); - } - } - - return availableLayers.concat(globalLayers); - } - - private static LoadRasterIndex(): BaseLayer[] { - const layers: BaseLayer[] = [] - // @ts-ignore - const features = editorlayerindex.features; - for (const i in features) { - const layer = features[i]; - const props = layer.properties; - - if (props.id === "Bing") { - // Doesnt work - continue; - } - - if (props.id === "MAPNIK") { - // Already added by default - continue; - } - - if (props.overlay) { - continue; - } - - if (props.url.toLowerCase().indexOf("apikey") > 0) { - continue; - } - - if (props.max_zoom < 19) { - // We want users to zoom to level 19 when adding a point - // If they are on a layer which hasn't enough precision, they can not zoom far enough. This is confusing, so we don't use this layer - continue; - } - - if (props.name === undefined) { - console.warn("Editor layer index: name not defined on ", props) - continue - } - - const leafletLayer: () => TileLayer = () => AvailableBaseLayers.CreateBackgroundLayer( - props.id, - props.name, - props.url, - props.name, - props.license_url, - props.max_zoom, - props.type.toLowerCase() === "wms", - props.type.toLowerCase() === "wmts" - ) - - // Note: if layer.geometry is null, there is global coverage for this layer - layers.push({ - id: props.id, - max_zoom: props.max_zoom ?? 25, - min_zoom: props.min_zoom ?? 1, - name: props.name, - layer: leafletLayer, - feature: layer, - isBest: props.best ?? false, - category: props.category - }); - } - return layers; - } - - private static LoadProviderIndex(): BaseLayer[] { - // @ts-ignore - X; // Import X to make sure the namespace is not optimized away - function l(id: string, name: string): BaseLayer { - try { - const layer: any = () => L.tileLayer.provider(id, undefined); - return { - feature: null, - id: id, - name: name, - layer: layer, - min_zoom: layer.minzoom, - max_zoom: layer.maxzoom, - category: "osmbasedmap", - isBest: false - } - } catch (e) { - console.error("Could not find provided layer", name, e); - return null; - } - } - - const layers = [ - l("CyclOSM", "CyclOSM - A bicycle oriented map"), - l("Stamen.TonerLite", "Toner Lite (by Stamen)"), - l("Stamen.TonerBackground", "Toner Background - no labels (by Stamen)"), - l("Stamen.Watercolor", "Watercolor (by Stamen)"), - l("Stadia.OSMBright", "Osm Bright (by Stadia)"), - l("CartoDB.Positron", "Positron (by CartoDB)"), - l("CartoDB.PositronNoLabels", "Positron - no labels (by CartoDB)"), - l("CartoDB.Voyager", "Voyager (by CartoDB)"), - l("CartoDB.VoyagerNoLabels", "Voyager - no labels (by CartoDB)"), - ]; - return Utils.NoNull(layers); + static SelectBestLayerAccordingTo(location: UIEventSource, preferedCategory: UIEventSource): UIEventSource { + return AvailableBaseLayers.implementation.SelectBestLayerAccordingTo(location, preferedCategory); } - /** - * Converts a layer from the editor-layer-index into a tilelayer usable by leaflet - */ - private static CreateBackgroundLayer(id: string, name: string, url: string, attribution: string, attributionUrl: string, - maxZoom: number, isWms: boolean, isWMTS?: boolean): TileLayer { - - url = url.replace("{zoom}", "{z}") - .replace("&BBOX={bbox}", "") - .replace("&bbox={bbox}", ""); - - const subdomainsMatch = url.match(/{switch:[^}]*}/) - let domains: string[] = []; - if (subdomainsMatch !== null) { - let domainsStr = subdomainsMatch[0].substr("{switch:".length); - domainsStr = domainsStr.substr(0, domainsStr.length - 1); - domains = domainsStr.split(","); - url = url.replace(/{switch:[^}]*}/, "{s}") - } - - - if (isWms) { - url = url.replace("&SRS={proj}", ""); - url = url.replace("&srs={proj}", ""); - const paramaters = ["format", "layers", "version", "service", "request", "styles", "transparent", "version"]; - const urlObj = new URL(url); - - const isUpper = urlObj.searchParams["LAYERS"] !== null; - const options = { - maxZoom: maxZoom ?? 19, - attribution: attribution + " | ", - subdomains: domains, - uppercase: isUpper, - transparent: false - }; - - for (const paramater of paramaters) { - let p = paramater; - if (isUpper) { - p = paramater.toUpperCase(); - } - options[paramater] = urlObj.searchParams.get(p); - } - - if (options.transparent === null) { - options.transparent = false; - } - - - return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options); - } - - if (attributionUrl) { - attribution = `${attribution}`; - } - - return L.tileLayer(url, - { - attribution: attribution, - maxZoom: maxZoom, - minZoom: 1, - // @ts-ignore - wmts: isWMTS ?? false, - subdomains: domains - }); + public static implement(backend: AvailableBaseLayersObj){ + AvailableBaseLayers.layerOverview = backend.layerOverview + AvailableBaseLayers.osmCarto = backend.osmCarto + AvailableBaseLayers.implementation = backend } - } \ No newline at end of file diff --git a/Logic/Actors/AvailableBaseLayersImplementation.ts b/Logic/Actors/AvailableBaseLayersImplementation.ts new file mode 100644 index 0000000000..5d7116381a --- /dev/null +++ b/Logic/Actors/AvailableBaseLayersImplementation.ts @@ -0,0 +1,292 @@ +import BaseLayer from "../../Models/BaseLayer"; +import {UIEventSource} from "../UIEventSource"; +import Loc from "../../Models/Loc"; +import {GeoOperations} from "../GeoOperations"; +import * as editorlayerindex from "../../assets/editor-layer-index.json"; +import {TileLayer} from "leaflet"; +import * as X from "leaflet-providers"; +import * as L from "leaflet"; +import {Utils} from "../../Utils"; +import {AvailableBaseLayersObj} from "./AvailableBaseLayers"; + +export default class AvailableBaseLayersImplementation implements AvailableBaseLayersObj{ + + public readonly osmCarto: BaseLayer = + { + id: "osm", + name: "OpenStreetMap", + layer: () => AvailableBaseLayersImplementation.CreateBackgroundLayer("osm", "OpenStreetMap", + "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright", + 19, + false, false), + feature: null, + max_zoom: 19, + min_zoom: 0, + isBest: false, // This is a lie! Of course OSM is the best map! (But not in this context) + category: "osmbasedmap" + } + + public layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex()); + + public AvailableLayersAt(location: UIEventSource): UIEventSource { + const source = location.map( + (currentLocation) => { + + if (currentLocation === undefined) { + return this.layerOverview; + } + + const currentLayers = source?.data; // A bit unorthodox - I know + const newLayers = this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat); + + if (currentLayers === undefined) { + return newLayers; + } + if (newLayers.length !== currentLayers.length) { + return newLayers; + } + for (let i = 0; i < newLayers.length; i++) { + if (newLayers[i].name !== currentLayers[i].name) { + return newLayers; + } + } + + return currentLayers; + }); + return source; + } + + public SelectBestLayerAccordingTo(location: UIEventSource, preferedCategory: UIEventSource): UIEventSource { + return this.AvailableLayersAt(location).map(available => { + // First float all 'best layers' to the top + available.sort((a, b) => { + if (a.isBest && b.isBest) { + return 0; + } + if (!a.isBest) { + return 1 + } + + return -1; + } + ) + + if (preferedCategory.data === undefined) { + return available[0] + } + + let prefered: string [] + if (typeof preferedCategory.data === "string") { + prefered = [preferedCategory.data] + } else { + prefered = preferedCategory.data; + } + + prefered.reverse(); + for (const category of prefered) { + //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top + available.sort((a, b) => { + if (a.category === category && b.category === category) { + return 0; + } + if (a.category !== category) { + return 1 + } + + return -1; + } + ) + } + return available[0] + }) + } + + private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] { + const availableLayers = [this.osmCarto] + const globalLayers = []; + for (const layerOverviewItem of this.layerOverview) { + const layer = layerOverviewItem; + + if (layer.feature?.geometry === undefined || layer.feature?.geometry === null) { + globalLayers.push(layer); + continue; + } + + if (lon === undefined || lat === undefined) { + continue; + } + + if (GeoOperations.inside([lon, lat], layer.feature)) { + availableLayers.push(layer); + } + } + + return availableLayers.concat(globalLayers); + } + + private static LoadRasterIndex(): BaseLayer[] { + const layers: BaseLayer[] = [] + // @ts-ignore + const features = editorlayerindex.features; + for (const i in features) { + const layer = features[i]; + const props = layer.properties; + + if (props.id === "Bing") { + // Doesnt work + continue; + } + + if (props.id === "MAPNIK") { + // Already added by default + continue; + } + + if (props.overlay) { + continue; + } + + if (props.url.toLowerCase().indexOf("apikey") > 0) { + continue; + } + + if (props.max_zoom < 19) { + // We want users to zoom to level 19 when adding a point + // If they are on a layer which hasn't enough precision, they can not zoom far enough. This is confusing, so we don't use this layer + continue; + } + + if (props.name === undefined) { + console.warn("Editor layer index: name not defined on ", props) + continue + } + + const leafletLayer: () => TileLayer = () => AvailableBaseLayersImplementation.CreateBackgroundLayer( + props.id, + props.name, + props.url, + props.name, + props.license_url, + props.max_zoom, + props.type.toLowerCase() === "wms", + props.type.toLowerCase() === "wmts" + ) + + // Note: if layer.geometry is null, there is global coverage for this layer + layers.push({ + id: props.id, + max_zoom: props.max_zoom ?? 25, + min_zoom: props.min_zoom ?? 1, + name: props.name, + layer: leafletLayer, + feature: layer, + isBest: props.best ?? false, + category: props.category + }); + } + return layers; + } + + private static LoadProviderIndex(): BaseLayer[] { + // @ts-ignore + X; // Import X to make sure the namespace is not optimized away + function l(id: string, name: string): BaseLayer { + try { + const layer: any = () => L.tileLayer.provider(id, undefined); + return { + feature: null, + id: id, + name: name, + layer: layer, + min_zoom: layer.minzoom, + max_zoom: layer.maxzoom, + category: "osmbasedmap", + isBest: false + } + } catch (e) { + console.error("Could not find provided layer", name, e); + return null; + } + } + + const layers = [ + l("CyclOSM", "CyclOSM - A bicycle oriented map"), + l("Stamen.TonerLite", "Toner Lite (by Stamen)"), + l("Stamen.TonerBackground", "Toner Background - no labels (by Stamen)"), + l("Stamen.Watercolor", "Watercolor (by Stamen)"), + l("Stadia.OSMBright", "Osm Bright (by Stadia)"), + l("CartoDB.Positron", "Positron (by CartoDB)"), + l("CartoDB.PositronNoLabels", "Positron - no labels (by CartoDB)"), + l("CartoDB.Voyager", "Voyager (by CartoDB)"), + l("CartoDB.VoyagerNoLabels", "Voyager - no labels (by CartoDB)"), + ]; + return Utils.NoNull(layers); + + } + + /** + * Converts a layer from the editor-layer-index into a tilelayer usable by leaflet + */ + private static CreateBackgroundLayer(id: string, name: string, url: string, attribution: string, attributionUrl: string, + maxZoom: number, isWms: boolean, isWMTS?: boolean): TileLayer { + + url = url.replace("{zoom}", "{z}") + .replace("&BBOX={bbox}", "") + .replace("&bbox={bbox}", ""); + + const subdomainsMatch = url.match(/{switch:[^}]*}/) + let domains: string[] = []; + if (subdomainsMatch !== null) { + let domainsStr = subdomainsMatch[0].substr("{switch:".length); + domainsStr = domainsStr.substr(0, domainsStr.length - 1); + domains = domainsStr.split(","); + url = url.replace(/{switch:[^}]*}/, "{s}") + } + + + if (isWms) { + url = url.replace("&SRS={proj}", ""); + url = url.replace("&srs={proj}", ""); + const paramaters = ["format", "layers", "version", "service", "request", "styles", "transparent", "version"]; + const urlObj = new URL(url); + + const isUpper = urlObj.searchParams["LAYERS"] !== null; + const options = { + maxZoom: maxZoom ?? 19, + attribution: attribution + " | ", + subdomains: domains, + uppercase: isUpper, + transparent: false + }; + + for (const paramater of paramaters) { + let p = paramater; + if (isUpper) { + p = paramater.toUpperCase(); + } + options[paramater] = urlObj.searchParams.get(p); + } + + if (options.transparent === null) { + options.transparent = false; + } + + + return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options); + } + + if (attributionUrl) { + attribution = `${attribution}`; + } + + return L.tileLayer(url, + { + attribution: attribution, + maxZoom: maxZoom, + minZoom: 1, + // @ts-ignore + wmts: isWMTS ?? false, + subdomains: domains + }); + } +} \ No newline at end of file diff --git a/Logic/Actors/OverpassFeatureSource.ts b/Logic/Actors/OverpassFeatureSource.ts index 62cf799988..5062133b68 100644 --- a/Logic/Actors/OverpassFeatureSource.ts +++ b/Logic/Actors/OverpassFeatureSource.ts @@ -179,7 +179,7 @@ export default class OverpassFeatureSource implements FeatureSource { self.retries.setData(0); try { - data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date)); + data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date, undefined)); self.features.setData(data.features.map(f => ({feature: f, freshness: date}))); return [bounds, date, layersToDownload]; } catch (e) { diff --git a/Logic/Actors/StrayClickHandler.ts b/Logic/Actors/StrayClickHandler.ts index 1cde818717..bdf79d162a 100644 --- a/Logic/Actors/StrayClickHandler.ts +++ b/Logic/Actors/StrayClickHandler.ts @@ -80,5 +80,6 @@ export default class StrayClickHandler { } + } \ No newline at end of file diff --git a/Logic/State/FeaturePipelineState.ts b/Logic/State/FeaturePipelineState.ts index 33124f8441..d20800035f 100644 --- a/Logic/State/FeaturePipelineState.ts +++ b/Logic/State/FeaturePipelineState.ts @@ -121,50 +121,7 @@ export default class FeaturePipelineState extends MapState { }) } - public setupClickDialogOnMap(filterViewIsOpened: UIEventSource, leafletMap: UIEventSource) { - - 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(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; - } - }) - - } + diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts index bf3422762a..afcbb1bf21 100644 --- a/Logic/State/MapState.ts +++ b/Logic/State/MapState.ts @@ -7,9 +7,6 @@ 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"; @@ -26,7 +23,7 @@ export default class MapState extends UserRelatedState { /** The leaflet instance of the big basemap */ - public leafletMap = new UIEventSource(undefined, "leafletmap"); + public leafletMap = new UIEventSource(undefined, "leafletmap"); /** * A list of currently available background layers */ @@ -67,7 +64,6 @@ export default class MapState extends UserRelatedState { */ public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource }[] - constructor(layoutToUse: LayoutConfig) { super(layoutToUse); @@ -120,57 +116,19 @@ export default class MapState extends UserRelatedState { 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; @@ -198,6 +156,7 @@ export default class MapState extends UserRelatedState { }) } } + private InitializeFilteredLayers() { // Initialize the filtered layers state @@ -252,8 +211,8 @@ export default class MapState extends UserRelatedState { return new UIEventSource(flayers); } - public AddAllOverlaysToMap(leafletMap: UIEventSource){ - const initialized =new Set() + public AddAllOverlaysToMap(leafletMap: UIEventSource) { + const initialized = new Set() for (const overlayToggle of this.overlayToggles) { new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed) initialized.add(overlayToggle.config) diff --git a/Logic/State/UserRelatedState.ts b/Logic/State/UserRelatedState.ts index a010fa102e..e431bb7f79 100644 --- a/Logic/State/UserRelatedState.ts +++ b/Logic/State/UserRelatedState.ts @@ -9,6 +9,8 @@ import {Utils} from "../../Utils"; import Locale from "../../UI/i18n/Locale"; import ElementsState from "./ElementsState"; import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater"; +import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"; +import FeatureSource from "../FeatureSource/FeatureSource"; /** * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, @@ -34,6 +36,10 @@ export default class UserRelatedState extends ElementsState { * WHich other themes the user previously visited */ public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; + /** + * A feature source containing the current home location of the user + */ + public homeLocation: FeatureSource constructor(layoutToUse: LayoutConfig) { super(layoutToUse); @@ -104,4 +110,37 @@ export default class UserRelatedState extends ElementsState { .ping(); } + private initHomeLocation() { + const empty = [] + const feature = UIEventSource.ListStabilized(this.osmConnection.userDetails.map(userDetails => { + + if (userDetails === undefined) { + return undefined; + } + const home = userDetails.home; + if (home === undefined) { + return undefined; + } + return [home.lon, home.lat] + })).map(homeLonLat => { + if(homeLonLat === undefined){ + return empty + } + return [{ + "type": "Feature", + "properties": { + "user:home": "yes", + "_lon": homeLonLat[0], + "_lat": homeLonLat[1] + }, + "geometry": { + "type": "Point", + "coordinates": homeLonLat + } + }] + }) + + this.homeLocation = new StaticFeatureSource(feature, false) + } + } \ No newline at end of file diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index d597bd5727..77431441f9 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -136,7 +136,7 @@ export class UIEventSource { if (oldList === list) { return; } - if (oldList.length !== list.length) { + if (oldList === undefined || oldList.length !== list.length) { stable.setData(list); return; } diff --git a/Models/ThemeConfig/Json/LayoutConfigJson.ts b/Models/ThemeConfig/Json/LayoutConfigJson.ts index e2df334ca5..be7c3ccf53 100644 --- a/Models/ThemeConfig/Json/LayoutConfigJson.ts +++ b/Models/ThemeConfig/Json/LayoutConfigJson.ts @@ -117,7 +117,7 @@ export interface LayoutConfigJson { * These tiles are using a ceratin zoom level, that can be controlled here * Default: overpassMaxZoom + 1 */ - osmApiTileSize: number + osmApiTileSize?: number /** * A tagrendering depicts how to show some tags or how to show a question for it. diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index e7273c29f6..da15f1b2a3 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -17,6 +17,12 @@ import {VariableUiElement} from "./Base/VariableUIElement"; import LeftControls from "./BigComponents/LeftControls"; import RightControls from "./BigComponents/RightControls"; import CenterMessageBox from "./CenterMessageBox"; +import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; +import AllKnownLayers from "../Customizations/AllKnownLayers"; +import ScrollableFullScreen from "./Base/ScrollableFullScreen"; +import Translations from "./i18n/Translations"; +import SimpleAddUI from "./BigComponents/SimpleAddUI"; +import StrayClickHandler from "../Logic/Actors/StrayClickHandler"; export class DefaultGuiState { public readonly welcomeMessageIsOpened; @@ -85,9 +91,9 @@ export default class DefaultGUI { state.mainMapObject.SetClass("w-full h-full") .AttachTo("leafletDiv") - state.setupClickDialogOnMap( + this.setupClickDialogOnMap( guiState.filterViewIsOpened, - state.leafletMap + state ) this.InitWelcomeMessage(); @@ -125,6 +131,14 @@ export default class DefaultGUI { document .getElementById("centermessage") .classList.add("pointer-events-none"); + + + new ShowDataLayer({ + leafletMap: state.leafletMap, + layerToShow: AllKnownLayers.sharedLayers.get("home_location"), + features: state.homeLocation, + enablePopups: false, + }) } @@ -158,4 +172,48 @@ export default class DefaultGUI { } + public setupClickDialogOnMap(filterViewIsOpened: UIEventSource, state: FeaturePipelineState) { + + function setup(){ + let presetCount = 0; + for (const layer of state.layoutToUse.layers) { + for (const preset of layer.presets) { + presetCount++; + } + } + if (presetCount == 0) { + return; + } + + const newPointDialogIsShown = new UIEventSource(false); + const addNewPoint = new ScrollableFullScreen( + () => Translations.t.general.add.title.Clone(), + () => new SimpleAddUI(newPointDialogIsShown, filterViewIsOpened, state), + "new", + newPointDialogIsShown + ); + addNewPoint.isShown.addCallback((isShown) => { + if (!isShown) { + state.LastClickLocation.setData(undefined); + } + }); + + new StrayClickHandler( + state.LastClickLocation, + state.selectedElement, + state.filteredLayers, + state.leafletMap, + addNewPoint + ); + } + + state.featureSwitchAddNew.addCallbackAndRunD(addNewAllowed => { + if (addNewAllowed) { + setup() + return true; + } + }) + + } + } \ No newline at end of file diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 9840071aa6..ad55543462 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -18,6 +18,7 @@ import {Unit} from "../../Models/Unit"; import {FixedInputElement} from "./FixedInputElement"; import WikidataSearchBox from "../Wikipedia/WikidataSearchBox"; import Wikidata from "../../Logic/Web/Wikidata"; +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; interface TextFieldDef { name: string, @@ -35,8 +36,6 @@ interface TextFieldDef { export default class ValidatedTextField { - public static bestLayerAt: (location: UIEventSource, preferences: UIEventSource) => any - public static tpList: TextFieldDef[] = [ ValidatedTextField.tp( "string", @@ -93,7 +92,7 @@ export default class ValidatedTextField { }) if (args[1]) { // We have a prefered map! - options.mapBackgroundLayer = ValidatedTextField.bestLayerAt( + options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( location, new UIEventSource(args[1].split(",")) ) } @@ -137,7 +136,7 @@ export default class ValidatedTextField { }) if (args[1]) { // We have a prefered map! - options.mapBackgroundLayer = ValidatedTextField.bestLayerAt( + options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( location, new UIEventSource(args[1].split(",")) ) } diff --git a/UI/Popup/MoveWizard.ts b/UI/Popup/MoveWizard.ts index fa3966cba1..3606581e2b 100644 --- a/UI/Popup/MoveWizard.ts +++ b/UI/Popup/MoveWizard.ts @@ -18,6 +18,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import MoveConfig from "../../Models/ThemeConfig/MoveConfig"; import {ElementStorage} from "../../Logic/ElementStorage"; import ValidatedTextField from "../Input/ValidatedTextField"; +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; interface MoveReason { text: Translation | string, @@ -138,7 +139,7 @@ export default class MoveWizard extends Toggle { const locationInput = new LocationInput({ minZoom: reason.minZoom, centerLocation: loc, - mapBackground: ValidatedTextField.bestLayerAt(loc, new UIEventSource(background)) + mapBackground:AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource(background)) }) if (reason.lockBounds) { diff --git a/UI/ShowDataLayer/ShowOverlayLayer.ts b/UI/ShowDataLayer/ShowOverlayLayer.ts index 0756c5e023..e75d263906 100644 --- a/UI/ShowDataLayer/ShowOverlayLayer.ts +++ b/UI/ShowDataLayer/ShowOverlayLayer.ts @@ -3,43 +3,17 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import * as L from "leaflet"; export default class ShowOverlayLayer { + + public static implementation: (config: TilesourceConfig, + leafletMap: UIEventSource, + isShown?: UIEventSource) => void; constructor(config: TilesourceConfig, leafletMap: UIEventSource, isShown: UIEventSource = undefined) { - - leafletMap.map(leaflet => { - if(leaflet === undefined){ - return; - } - - const tileLayer = L.tileLayer(config.source, - { - attribution: "", - maxZoom: config.maxzoom, - minZoom: config.minzoom, - // @ts-ignore - wmts: false, - }); - - if(isShown === undefined){ - tileLayer.addTo(leaflet) - } - - isShown?.addCallbackAndRunD(isShown => { - if(isShown){ - tileLayer.addTo(leaflet) - }else{ - leaflet.removeLayer(tileLayer) - } - - }) - - } ) - - - + if(ShowOverlayLayer.implementation === undefined){ + throw "Call ShowOverlayLayerImplemenation.initialize() first before using this" + } + ShowOverlayLayer.implementation(config, leafletMap, isShown) } - - } \ No newline at end of file diff --git a/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts b/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts new file mode 100644 index 0000000000..ef92aef4eb --- /dev/null +++ b/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts @@ -0,0 +1,45 @@ +import * as L from "leaflet"; +import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import ShowOverlayLayer from "./ShowOverlayLayer"; + +export default class ShowOverlayLayerImplementation { + + public static Implement(){ + ShowOverlayLayer.implementation = ShowOverlayLayerImplementation.AddToMap + } + + public static AddToMap(config: TilesourceConfig, + leafletMap: UIEventSource, + isShown: UIEventSource = undefined){ + leafletMap.map(leaflet => { + if (leaflet === undefined) { + return; + } + + const tileLayer = L.tileLayer(config.source, + { + attribution: "", + maxZoom: config.maxzoom, + minZoom: config.minzoom, + // @ts-ignore + wmts: false, + }); + + if (isShown === undefined) { + tileLayer.addTo(leaflet) + } + + isShown?.addCallbackAndRunD(isShown => { + if (isShown) { + tileLayer.addTo(leaflet) + } else { + leaflet.removeLayer(tileLayer) + } + + }) + + }) + } + +} \ No newline at end of file diff --git a/assets/layers/home_location/home_location.json b/assets/layers/home_location/home_location.json new file mode 100644 index 0000000000..787b5ea67d --- /dev/null +++ b/assets/layers/home_location/home_location.json @@ -0,0 +1,17 @@ +{ + "id": "home_location", + "description": "Meta layer showing the home location of the user", + "minzoom": 0, + "source": { + "osmTags": "user:home=yes" + }, + "icon": { + "render": "circle:white;./assets/svg/home.svg" + }, + "iconSize": { + "render": "20,20,center" + }, + "color": { + "render": "#00f" + } +} \ No newline at end of file diff --git a/index.ts b/index.ts index 027cb1b56b..61ebf59e69 100644 --- a/index.ts +++ b/index.ts @@ -12,11 +12,16 @@ import DetermineLayout from "./Logic/DetermineLayout"; import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; import DefaultGUI, {DefaultGuiState} from "./UI/DefaultGUI"; import State from "./State"; +import AvailableBaseLayersImplementation from "./Logic/Actors/AvailableBaseLayersImplementation"; +import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation"; +// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts running from console 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) +AvailableBaseLayers.implement(new AvailableBaseLayersImplementation()) SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); +ShowOverlayLayerImplementation.Implement(); +// Miscelleanous + Utils.DisableLongPresses() // --------------------- Special actions based on the parameters -----------------