forked from MapComplete/MapComplete
		
	More refactoring to fix the tests
This commit is contained in:
		
							parent
							
								
									71285d34cd
								
							
						
					
					
						commit
						b8abbc9505
					
				
					 16 changed files with 507 additions and 418 deletions
				
			
		|  | @ -1,13 +1,15 @@ | ||||||
| import * as editorlayerindex from "../../assets/editor-layer-index.json" |  | ||||||
| import BaseLayer from "../../Models/BaseLayer"; | 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 {UIEventSource} from "../UIEventSource"; | ||||||
| import {GeoOperations} from "../GeoOperations"; |  | ||||||
| import {Utils} from "../../Utils"; |  | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc"; | ||||||
| 
 | 
 | ||||||
|  | export interface AvailableBaseLayersObj { | ||||||
|  |     readonly osmCarto: BaseLayer; | ||||||
|  |     layerOverview: BaseLayer[]; | ||||||
|  |     AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]>  | ||||||
|  |     SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> ; | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Calculates which layers are available at the current location |  * Calculates which layers are available at the current location | ||||||
|  * Changes the basemap |  * Changes the basemap | ||||||
|  | @ -15,284 +17,24 @@ import Loc from "../../Models/Loc"; | ||||||
| export default class AvailableBaseLayers { | export default class AvailableBaseLayers { | ||||||
|      |      | ||||||
|      |      | ||||||
|     public static osmCarto: BaseLayer = |     public static layerOverview: BaseLayer[]; | ||||||
|         { |     public static osmCarto: BaseLayer; | ||||||
|             id: "osm", | 
 | ||||||
|             name: "OpenStreetMap", |     private static implementation: AvailableBaseLayersObj | ||||||
|             layer: () => AvailableBaseLayers.CreateBackgroundLayer("osm", "OpenStreetMap", |      | ||||||
|                 "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright", |     static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> { | ||||||
|                 19, |         return AvailableBaseLayers.implementation.AvailableLayersAt(location); | ||||||
|                 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()); |     static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> { | ||||||
| 
 |         return AvailableBaseLayers.implementation.SelectBestLayerAccordingTo(location, preferedCategory); | ||||||
|     public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> { |  | ||||||
|         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; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> { |  | ||||||
|         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); |  | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     public static implement(backend: AvailableBaseLayersObj){ | ||||||
|      * Converts a layer from the editor-layer-index into a tilelayer usable by leaflet |         AvailableBaseLayers.layerOverview = backend.layerOverview | ||||||
|      */ |         AvailableBaseLayers.osmCarto = backend.osmCarto | ||||||
|     private static CreateBackgroundLayer(id: string, name: string, url: string, attribution: string, attributionUrl: string, |         AvailableBaseLayers.implementation = backend | ||||||
|                                          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 = `<a href='${attributionUrl}' target='_blank'>${attribution}</a>`; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return L.tileLayer(url, |  | ||||||
|             { |  | ||||||
|                 attribution: attribution, |  | ||||||
|                 maxZoom: maxZoom, |  | ||||||
|                 minZoom: 1, |  | ||||||
|                 // @ts-ignore
 |  | ||||||
|                 wmts: isWMTS ?? false, |  | ||||||
|                 subdomains: domains |  | ||||||
|             }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
							
								
								
									
										292
									
								
								Logic/Actors/AvailableBaseLayersImplementation.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								Logic/Actors/AvailableBaseLayersImplementation.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<Loc>): UIEventSource<BaseLayer[]> { | ||||||
|  |         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<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> { | ||||||
|  |         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 = `<a href='${attributionUrl}' target='_blank'>${attribution}</a>`; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return L.tileLayer(url, | ||||||
|  |             { | ||||||
|  |                 attribution: attribution, | ||||||
|  |                 maxZoom: maxZoom, | ||||||
|  |                 minZoom: 1, | ||||||
|  |                 // @ts-ignore
 | ||||||
|  |                 wmts: isWMTS ?? false, | ||||||
|  |                 subdomains: domains | ||||||
|  |             }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -179,7 +179,7 @@ export default class OverpassFeatureSource implements FeatureSource { | ||||||
| 
 | 
 | ||||||
|         self.retries.setData(0); |         self.retries.setData(0); | ||||||
|         try { |         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}))); |             self.features.setData(data.features.map(f => ({feature: f, freshness: date}))); | ||||||
|             return [bounds, date, layersToDownload]; |             return [bounds, date, layersToDownload]; | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|  |  | ||||||
|  | @ -81,4 +81,5 @@ export default class StrayClickHandler { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|      |      | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -121,50 +121,7 @@ export default class FeaturePipelineState extends MapState { | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     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; |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,9 +7,6 @@ import BackgroundLayerResetter from "../Actors/BackgroundLayerResetter"; | ||||||
| import Attribution from "../../UI/BigComponents/Attribution"; | import Attribution from "../../UI/BigComponents/Attribution"; | ||||||
| import Minimap, {MinimapObj} from "../../UI/Base/Minimap"; | import Minimap, {MinimapObj} from "../../UI/Base/Minimap"; | ||||||
| import {Tiles} from "../../Models/TileRange"; | 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 BaseUIElement from "../../UI/BaseUIElement"; | ||||||
| import FilteredLayer from "../../Models/FilteredLayer"; | import FilteredLayer from "../../Models/FilteredLayer"; | ||||||
| import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; | import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; | ||||||
|  | @ -26,7 +23,7 @@ export default class MapState extends UserRelatedState { | ||||||
|     /** |     /** | ||||||
|      The leaflet instance of the big basemap |      The leaflet instance of the big basemap | ||||||
|      */ |      */ | ||||||
|     public leafletMap = new UIEventSource<L.Map>(undefined, "leafletmap"); |     public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap"); | ||||||
|     /** |     /** | ||||||
|      * A list of currently available background layers |      * A list of currently available background layers | ||||||
|      */ |      */ | ||||||
|  | @ -68,7 +65,6 @@ export default class MapState extends UserRelatedState { | ||||||
|     public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[] |     public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     constructor(layoutToUse: LayoutConfig) { |     constructor(layoutToUse: LayoutConfig) { | ||||||
|         super(layoutToUse); |         super(layoutToUse); | ||||||
| 
 | 
 | ||||||
|  | @ -130,47 +126,9 @@ export default class MapState extends UserRelatedState { | ||||||
| 
 | 
 | ||||||
|         this.lockBounds() |         this.lockBounds() | ||||||
|         this.AddAllOverlaysToMap(this.leafletMap) |         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() { |     private lockBounds() { | ||||||
|         const layout = this.layoutToUse; |         const layout = this.layoutToUse; | ||||||
|  | @ -198,6 +156,7 @@ export default class MapState extends UserRelatedState { | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     private InitializeFilteredLayers() { |     private InitializeFilteredLayers() { | ||||||
|         // Initialize the filtered layers state
 |         // Initialize the filtered layers state
 | ||||||
| 
 | 
 | ||||||
|  | @ -252,8 +211,8 @@ export default class MapState extends UserRelatedState { | ||||||
|         return new UIEventSource<FilteredLayer[]>(flayers); |         return new UIEventSource<FilteredLayer[]>(flayers); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public AddAllOverlaysToMap(leafletMap: UIEventSource<any>){ |     public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) { | ||||||
|         const initialized =new Set() |         const initialized = new Set() | ||||||
|         for (const overlayToggle of this.overlayToggles) { |         for (const overlayToggle of this.overlayToggles) { | ||||||
|             new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed) |             new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed) | ||||||
|             initialized.add(overlayToggle.config) |             initialized.add(overlayToggle.config) | ||||||
|  |  | ||||||
|  | @ -9,6 +9,8 @@ import {Utils} from "../../Utils"; | ||||||
| import Locale from "../../UI/i18n/Locale"; | import Locale from "../../UI/i18n/Locale"; | ||||||
| import ElementsState from "./ElementsState"; | import ElementsState from "./ElementsState"; | ||||||
| import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater"; | 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, |  * 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 |      * WHich other themes the user previously visited | ||||||
|      */ |      */ | ||||||
|     public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; |     public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; | ||||||
|  |     /** | ||||||
|  |      * A feature source containing the current home location of the user | ||||||
|  |      */ | ||||||
|  |     public homeLocation: FeatureSource | ||||||
| 
 | 
 | ||||||
|     constructor(layoutToUse: LayoutConfig) { |     constructor(layoutToUse: LayoutConfig) { | ||||||
|         super(layoutToUse); |         super(layoutToUse); | ||||||
|  | @ -104,4 +110,37 @@ export default class UserRelatedState extends ElementsState { | ||||||
|             .ping(); |             .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) | ||||||
|  |     } | ||||||
|  |      | ||||||
| } | } | ||||||
|  | @ -136,7 +136,7 @@ export class UIEventSource<T> { | ||||||
|             if (oldList === list) { |             if (oldList === list) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             if (oldList.length !== list.length) { |             if (oldList === undefined || oldList.length !== list.length) { | ||||||
|                 stable.setData(list); |                 stable.setData(list); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -117,7 +117,7 @@ export interface LayoutConfigJson { | ||||||
|      * These tiles are using a ceratin zoom level, that can be controlled here |      * These tiles are using a ceratin zoom level, that can be controlled here | ||||||
|      * Default: overpassMaxZoom + 1 |      * Default: overpassMaxZoom + 1 | ||||||
|      */ |      */ | ||||||
|     osmApiTileSize: number |     osmApiTileSize?: number | ||||||
|      |      | ||||||
|     /** |     /** | ||||||
|      * A tagrendering depicts how to show some tags or how to show a question for it. |      * A tagrendering depicts how to show some tags or how to show a question for it. | ||||||
|  |  | ||||||
|  | @ -17,6 +17,12 @@ import {VariableUiElement} from "./Base/VariableUIElement"; | ||||||
| import LeftControls from "./BigComponents/LeftControls"; | import LeftControls from "./BigComponents/LeftControls"; | ||||||
| import RightControls from "./BigComponents/RightControls"; | import RightControls from "./BigComponents/RightControls"; | ||||||
| import CenterMessageBox from "./CenterMessageBox"; | 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 { | export class DefaultGuiState { | ||||||
|     public readonly welcomeMessageIsOpened; |     public readonly welcomeMessageIsOpened; | ||||||
|  | @ -85,9 +91,9 @@ export default class DefaultGUI { | ||||||
|         state.mainMapObject.SetClass("w-full h-full") |         state.mainMapObject.SetClass("w-full h-full") | ||||||
|             .AttachTo("leafletDiv") |             .AttachTo("leafletDiv") | ||||||
| 
 | 
 | ||||||
|         state.setupClickDialogOnMap( |         this.setupClickDialogOnMap( | ||||||
|             guiState.filterViewIsOpened, |             guiState.filterViewIsOpened, | ||||||
|             state.leafletMap |             state | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         this.InitWelcomeMessage(); |         this.InitWelcomeMessage(); | ||||||
|  | @ -126,6 +132,14 @@ export default class DefaultGUI { | ||||||
|             .getElementById("centermessage") |             .getElementById("centermessage") | ||||||
|             .classList.add("pointer-events-none"); |             .classList.add("pointer-events-none"); | ||||||
|          |          | ||||||
|  |          | ||||||
|  |         new ShowDataLayer({ | ||||||
|  |             leafletMap: state.leafletMap, | ||||||
|  |             layerToShow: AllKnownLayers.sharedLayers.get("home_location"), | ||||||
|  |             features:            state.homeLocation, | ||||||
|  |             enablePopups: false, | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private InitWelcomeMessage() { |     private InitWelcomeMessage() { | ||||||
|  | @ -158,4 +172,48 @@ export default class DefaultGUI { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public setupClickDialogOnMap(filterViewIsOpened: UIEventSource<boolean>, 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<boolean>(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; | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -18,6 +18,7 @@ import {Unit} from "../../Models/Unit"; | ||||||
| import {FixedInputElement} from "./FixedInputElement"; | import {FixedInputElement} from "./FixedInputElement"; | ||||||
| import WikidataSearchBox from "../Wikipedia/WikidataSearchBox"; | import WikidataSearchBox from "../Wikipedia/WikidataSearchBox"; | ||||||
| import Wikidata from "../../Logic/Web/Wikidata"; | import Wikidata from "../../Logic/Web/Wikidata"; | ||||||
|  | import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||||
| 
 | 
 | ||||||
| interface TextFieldDef { | interface TextFieldDef { | ||||||
|     name: string, |     name: string, | ||||||
|  | @ -35,8 +36,6 @@ interface TextFieldDef { | ||||||
| 
 | 
 | ||||||
| export default class ValidatedTextField { | export default class ValidatedTextField { | ||||||
| 
 | 
 | ||||||
|     public static bestLayerAt: (location: UIEventSource<Loc>, preferences: UIEventSource<string[]>) => any |  | ||||||
| 
 |  | ||||||
|     public static tpList: TextFieldDef[] = [ |     public static tpList: TextFieldDef[] = [ | ||||||
|         ValidatedTextField.tp( |         ValidatedTextField.tp( | ||||||
|             "string", |             "string", | ||||||
|  | @ -93,7 +92,7 @@ export default class ValidatedTextField { | ||||||
|                 }) |                 }) | ||||||
|                 if (args[1]) { |                 if (args[1]) { | ||||||
|                     // We have a prefered map!
 |                     // We have a prefered map!
 | ||||||
|                     options.mapBackgroundLayer = ValidatedTextField.bestLayerAt( |                     options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( | ||||||
|                         location, new UIEventSource<string[]>(args[1].split(",")) |                         location, new UIEventSource<string[]>(args[1].split(",")) | ||||||
|                     ) |                     ) | ||||||
|                 } |                 } | ||||||
|  | @ -137,7 +136,7 @@ export default class ValidatedTextField { | ||||||
|                 }) |                 }) | ||||||
|                 if (args[1]) { |                 if (args[1]) { | ||||||
|                     // We have a prefered map!
 |                     // We have a prefered map!
 | ||||||
|                     options.mapBackgroundLayer = ValidatedTextField.bestLayerAt( |                     options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( | ||||||
|                         location, new UIEventSource<string[]>(args[1].split(",")) |                         location, new UIEventSource<string[]>(args[1].split(",")) | ||||||
|                     ) |                     ) | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
| import MoveConfig from "../../Models/ThemeConfig/MoveConfig"; | import MoveConfig from "../../Models/ThemeConfig/MoveConfig"; | ||||||
| import {ElementStorage} from "../../Logic/ElementStorage"; | import {ElementStorage} from "../../Logic/ElementStorage"; | ||||||
| import ValidatedTextField from "../Input/ValidatedTextField"; | import ValidatedTextField from "../Input/ValidatedTextField"; | ||||||
|  | import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||||
| 
 | 
 | ||||||
| interface MoveReason { | interface MoveReason { | ||||||
|     text: Translation | string, |     text: Translation | string, | ||||||
|  | @ -138,7 +139,7 @@ export default class MoveWizard extends Toggle { | ||||||
|             const locationInput = new LocationInput({ |             const locationInput = new LocationInput({ | ||||||
|                 minZoom: reason.minZoom, |                 minZoom: reason.minZoom, | ||||||
|                 centerLocation: loc, |                 centerLocation: loc, | ||||||
|                 mapBackground: ValidatedTextField.bestLayerAt(loc, new UIEventSource(background)) |                 mapBackground:AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource(background)) | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|             if (reason.lockBounds) { |             if (reason.lockBounds) { | ||||||
|  |  | ||||||
|  | @ -4,42 +4,16 @@ import * as L from "leaflet"; | ||||||
| 
 | 
 | ||||||
| export default class ShowOverlayLayer { | export default class ShowOverlayLayer { | ||||||
| 
 | 
 | ||||||
|  |     public static implementation: (config: TilesourceConfig, | ||||||
|  |                                    leafletMap: UIEventSource<any>, | ||||||
|  |                                    isShown?: UIEventSource<boolean>) => void; | ||||||
|  |      | ||||||
|     constructor(config: TilesourceConfig, |     constructor(config: TilesourceConfig, | ||||||
|                 leafletMap: UIEventSource<any>, |                 leafletMap: UIEventSource<any>, | ||||||
|                 isShown: UIEventSource<boolean> = undefined) { |                 isShown: UIEventSource<boolean> = undefined) { | ||||||
|          |         if(ShowOverlayLayer.implementation === undefined){ | ||||||
|         leafletMap.map(leaflet => { |             throw "Call ShowOverlayLayerImplemenation.initialize() first before using this" | ||||||
|             if(leaflet === undefined){ |  | ||||||
|                 return; |  | ||||||
|         } |         } | ||||||
| 
 |             ShowOverlayLayer.implementation(config, leafletMap, isShown) | ||||||
|             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) |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|             }) |  | ||||||
|              |  | ||||||
|         } ) |  | ||||||
|          |  | ||||||
|          |  | ||||||
|          |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|      |  | ||||||
| } | } | ||||||
							
								
								
									
										45
									
								
								UI/ShowDataLayer/ShowOverlayLayerImplementation.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								UI/ShowDataLayer/ShowOverlayLayerImplementation.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<any>, | ||||||
|  |                            isShown: UIEventSource<boolean> = 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) | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |             }) | ||||||
|  | 
 | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								assets/layers/home_location/home_location.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								assets/layers/home_location/home_location.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -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" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										9
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -12,11 +12,16 @@ import DetermineLayout from "./Logic/DetermineLayout"; | ||||||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; | ||||||
| import DefaultGUI, {DefaultGuiState} from "./UI/DefaultGUI"; | import DefaultGUI, {DefaultGuiState} from "./UI/DefaultGUI"; | ||||||
| import State from "./State"; | 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() | MinimapImplementation.initialize() | ||||||
| // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
 | AvailableBaseLayers.implement(new AvailableBaseLayersImplementation()) | ||||||
| ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref) |  | ||||||
| SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | ||||||
|  | ShowOverlayLayerImplementation.Implement(); | ||||||
|  | // Miscelleanous
 | ||||||
|  | 
 | ||||||
| Utils.DisableLongPresses() | Utils.DisableLongPresses() | ||||||
| 
 | 
 | ||||||
| // --------------------- Special actions based on the parameters -----------------
 | // --------------------- Special actions based on the parameters -----------------
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue