forked from MapComplete/MapComplete
		
	First attempt to get the editor-layer-index working
This commit is contained in:
		
							parent
							
								
									e46ea51d44
								
							
						
					
					
						commit
						08175a747f
					
				
					 11 changed files with 268 additions and 74 deletions
				
			
		|  | @ -8,7 +8,7 @@ import {UIElement} from "./UI/UIElement"; | |||
| import {MoreScreen} from "./UI/MoreScreen"; | ||||
| import {FilteredLayer} from "./Logic/FilteredLayer"; | ||||
| import {FeatureInfoBox} from "./UI/FeatureInfoBox"; | ||||
| import {BaseLayers, Basemap} from "./Logic/Leaflet/Basemap"; | ||||
| import {Basemap} from "./Logic/Leaflet/Basemap"; | ||||
| import {State} from "./State"; | ||||
| import {WelcomeMessage} from "./UI/WelcomeMessage"; | ||||
| import {Img} from "./UI/Img"; | ||||
|  | @ -35,6 +35,7 @@ import {Layout} from "./Customizations/Layout"; | |||
| import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; | ||||
| import {FromJSON} from "./Customizations/JSON/FromJSON"; | ||||
| import {Utils} from "./Utils"; | ||||
| import BackgroundSelector from "./UI/BackgroundSelector"; | ||||
| 
 | ||||
| export class InitUiElements { | ||||
| 
 | ||||
|  | @ -352,14 +353,12 @@ export class InitUiElements { | |||
|     } | ||||
| 
 | ||||
|     private static GenerateLayerControlPanel() { | ||||
|         let baseLayerOptions = BaseLayers.baseLayers.map((layer) => { | ||||
|             return {value: layer, shown: layer.name} | ||||
|         }); | ||||
|         let layerControlPanel = new Combine( | ||||
|             [new DropDown(Translations.t.general.backgroundMap, baseLayerOptions, State.state.bm.CurrentLayer)]); | ||||
|         let layerControlPanel: UIElement = new BackgroundSelector(State.state); | ||||
|         layerControlPanel.SetStyle("margin:1em"); | ||||
|         layerControlPanel.onClick(() => {}); | ||||
|         if (State.state.filteredLayers.data.length > 1) { | ||||
|             const layerSelection = new LayerSelection(); | ||||
|             layerSelection.onClick(() => {}); | ||||
|             layerControlPanel = new Combine([layerSelection, "<br/>",layerControlPanel]); | ||||
|         } | ||||
|         return layerControlPanel; | ||||
|  | @ -444,8 +443,8 @@ export class InitUiElements { | |||
|         ); | ||||
|         State.state.bm = bm; | ||||
|         State.state.layerUpdater = new LayerUpdater(State.state); | ||||
|         const queryParam = QueryParameters.GetQueryParameter("background", State.state.layoutToUse.data.defaultBackground); | ||||
|         const queryParamMapped: UIEventSource<{ id: string, name: string, layer: any }> = | ||||
|         /*  const queryParam = QueryParameters.GetQueryParameter("background", State.state.layoutToUse.data.defaultBackground); | ||||
|       const queryParamMapped: UIEventSource<{ id: string, name: string, layer: any }> = | ||||
|             queryParam.map<{ id: string, name: string, layer: any }>((id) => { | ||||
|                 for (const layer of BaseLayers.baseLayers) { | ||||
|                     if (layer.id === id) { | ||||
|  | @ -457,7 +456,7 @@ export class InitUiElements { | |||
|                 return layerInfo.id | ||||
|             }); | ||||
| 
 | ||||
|         queryParamMapped.syncWith(bm.CurrentLayer); | ||||
|         queryParamMapped.syncWith(bm.CurrentLayer);*/ | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										116
									
								
								Logic/AvailableBaseLayers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								Logic/AvailableBaseLayers.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,116 @@ | |||
| import * as editorlayerindex from "../assets/editor-layer-index.json" | ||||
| import {UIEventSource} from "./UIEventSource"; | ||||
| import {GeoOperations} from "./GeoOperations"; | ||||
| import {State} from "../State"; | ||||
| import {Basemap} from "./Leaflet/Basemap"; | ||||
| 
 | ||||
| /** | ||||
|  * Calculates which layers are available at the current location | ||||
|  */ | ||||
| export default class AvailableBaseLayers { | ||||
| 
 | ||||
|     public static layerOverview = AvailableBaseLayers.LoadRasterIndex(); | ||||
|     public availableEditorLayers: UIEventSource<{ id: string, url: string, max_zoom: number, license_url: number, name: string, geometry: any, leafletLayer: any }[]>; | ||||
| 
 | ||||
|     constructor(state: State) { | ||||
|         const self = this; | ||||
|         this.availableEditorLayers = | ||||
|             state.locationControl.map( | ||||
|                 (currentLocation) => { | ||||
|                     const currentLayers = self.availableEditorLayers?.data; | ||||
|                     const newLayers = AvailableBaseLayers.AvailableLayersAt(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 currentLocation; | ||||
|                 }); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public static AvailableLayersAt(lon: number, lat: number): { url: string, max_zoom: number, license_url: number, name: string, geometry: any }[] { | ||||
|         const availableLayers = [] | ||||
|         const globalLayers = []; | ||||
|         for (const i in AvailableBaseLayers.layerOverview) { | ||||
|             const layer = AvailableBaseLayers.layerOverview[i]; | ||||
|             if (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(): { id: string, url: string, max_zoom: number, license_url: number, name: string, feature: any }[] { | ||||
|         const layers: { id: string, url: string, max_zoom: number, license_url: number, name: string, feature: any, leafletLayer: any }[] = [] | ||||
|         // @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.overlay) { | ||||
|                 continue; | ||||
|             } | ||||
|              | ||||
|             if(props.max_zoom < 19){ | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             if (props.url.toLowerCase().indexOf("apikey") > 0) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             if (props.url.toLowerCase().indexOf("{bbox}") > 0) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             const leafletLayer = Basemap.CreateBackgroundLayer( | ||||
|                 props.id, | ||||
|                 props.name, | ||||
|                 props.url, | ||||
|                 props.name, | ||||
|                 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, | ||||
|                 feature: layer, | ||||
|                 url: props.url, | ||||
|                 max_zoom: props.max_zoom, | ||||
|                 license_url: props.license_url, | ||||
|                 name: props.name, | ||||
|                 leafletLayer: leafletLayer | ||||
|             }); | ||||
|         } | ||||
|         return layers; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -36,7 +36,6 @@ export class GeoOperations { | |||
|             return false; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") { | ||||
| 
 | ||||
|             const poly = feature; | ||||
|  | @ -77,7 +76,7 @@ export class GeoOperations { | |||
| 
 | ||||
|         return false; | ||||
|     } | ||||
|     private static inside(pointCoordinate, feature): boolean { | ||||
|     public static inside(pointCoordinate, feature): boolean { | ||||
|         // ray-casting algorithm based on
 | ||||
|         // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,54 +5,46 @@ import {UIElement} from "../../UI/UIElement"; | |||
| 
 | ||||
| export class BaseLayers { | ||||
| 
 | ||||
|     public static readonly defaultLayer: { name: string, layer: any, id: string } = { | ||||
|         id: "osm", | ||||
|         name: "Kaart van OpenStreetMap", layer: L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", | ||||
|             { | ||||
|                 attribution: '', | ||||
|                 maxZoom: 19, | ||||
|                 minZoom: 1 | ||||
|             }) | ||||
|     }; | ||||
|     public static readonly baseLayers: { name: string, layer: any, id: string } [] = [ | ||||
| 
 | ||||
|         { | ||||
|             id: "aiv-latest", | ||||
|             name: "Luchtfoto Vlaanderen (recentste door AIV)", | ||||
|             layer: L.tileLayer("https://tile.informatievlaanderen.be/ws/raadpleegdiensten/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&" + | ||||
|                 "LAYER=omwrgbmrvl&STYLE=&FORMAT=image/png&tileMatrixSet=GoogleMapsVL&tileMatrix={z}&tileRow={y}&tileCol={x}", | ||||
|                 { | ||||
|                     // omwrgbmrvl
 | ||||
|                     attribution: 'Luchtfoto\'s van © AIV Vlaanderen (Laatste)  © AGIV', | ||||
|                     maxZoom: 22, | ||||
|                     minZoom: 1, | ||||
|                     wmts: true | ||||
|                 }) | ||||
|         }, | ||||
|         BaseLayers.defaultLayer, | ||||
|         { | ||||
|             id: "aiv-13-15", | ||||
|             name: "Luchtfoto Vlaanderen (2013-2015, door AIV)", | ||||
|             layer: L.tileLayer.wms('https://geoservices.informatievlaanderen.be/raadpleegdiensten/OGW/wms?s', | ||||
|                 { | ||||
|                     maxZoom: 22, | ||||
|                     layers: "OGWRGB13_15VL", | ||||
|                     attribution: "Luchtfoto's van © AIV Vlaanderen (2013-2015) | " | ||||
|                 }) | ||||
|         }, | ||||
|         { | ||||
|             id:"grb", | ||||
|             name: "Kaart Grootschalig ReferentieBestand Vlaanderen (GRB) door AIV", | ||||
|             layer: L.tileLayer("https://tile.informatievlaanderen.be/ws/raadpleegdiensten/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=grb_bsk&STYLE=&FORMAT=image/png&tileMatrixSet=GoogleMapsVL&tileMatrix={z}&tileCol={x}&tileRow={y}", | ||||
|                 { | ||||
|                     attribution: 'Achtergrond <i>Grootschalig ReferentieBestand</i>(GRB) © AGIV', | ||||
|                     maxZoom: 22, | ||||
|                     minZoom: 1, | ||||
|                     wmts: true | ||||
|                 }) | ||||
|         } | ||||
|     ] | ||||
|     ; | ||||
|     /*public static readonly baseLayers: { name: string, layer: any, id: string } [] = [ | ||||
|   | ||||
|          { | ||||
|              id: "aiv-latest", | ||||
|              name: "Luchtfoto Vlaanderen (recentste door AIV)", | ||||
|              layer: L.tileLayer("https://tile.informatievlaanderen.be/ws/raadpleegdiensten/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&" + | ||||
|                  "LAYER=omwrgbmrvl&STYLE=&FORMAT=image/png&tileMatrixSet=GoogleMapsVL&tileMatrix={z}&tileRow={y}&tileCol={x}", | ||||
|                  { | ||||
|                      // omwrgbmrvl
 | ||||
|                      attribution: 'Luchtfoto\'s van © AIV Vlaanderen (Laatste)  © AGIV', | ||||
|                      maxZoom: 22, | ||||
|                      minZoom: 1, | ||||
|                      wmts: true | ||||
|                  }) | ||||
|          }, | ||||
|          BaseLayers.defaultLayer, | ||||
|          { | ||||
|              id: "aiv-13-15", | ||||
|              name: "Luchtfoto Vlaanderen (2013-2015, door AIV)", | ||||
|              layer: L.tileLayer.wms('https://geoservices.informatievlaanderen.be/raadpleegdiensten/OGW/wms?s', | ||||
|                  { | ||||
|                      maxZoom: 22, | ||||
|                      layers: "OGWRGB13_15VL", | ||||
|                      attribution: "Luchtfoto's van © AIV Vlaanderen (2013-2015) | " | ||||
|                  }) | ||||
|          }, | ||||
|          { | ||||
|              id:"grb", | ||||
|              name: "Kaart Grootschalig ReferentieBestand Vlaanderen (GRB) door AIV", | ||||
|              layer: L.tileLayer("https://tile.informatievlaanderen.be/ws/raadpleegdiensten/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=grb_bsk&STYLE=&FORMAT=image/png&tileMatrixSet=GoogleMapsVL&tileMatrix={z}&tileCol={x}&tileRow={y}", | ||||
|                  { | ||||
|                      attribution: 'Achtergrond <i>Grootschalig ReferentieBestand</i>(GRB) © AGIV', | ||||
|                      maxZoom: 22, | ||||
|                      minZoom: 1, | ||||
|                      wmts: true | ||||
|                  }) | ||||
|          } | ||||
|      ] | ||||
|      ;*/ | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
|  | @ -60,26 +52,32 @@ export class BaseLayers { | |||
| export class Basemap { | ||||
| 
 | ||||
| 
 | ||||
|     public static readonly defaultLayer: { name: string, layer: any, id: string } = | ||||
|         Basemap.CreateBackgroundLayer("osm", "OpenStreetMap", "https://tile.openstreetmap.org/{z}/{x}/{y}.png", | ||||
|             "<a href='https://openstreetmap.org/copyright' target='_blank'>OpenStreetMap (ODBL)</a>", | ||||
|             22, false); | ||||
| 
 | ||||
|     // @ts-ignore
 | ||||
|     public readonly map: Map; | ||||
| 
 | ||||
|     public readonly Location: UIEventSource<{ zoom: number, lat: number, lon: number }>; | ||||
|     public readonly LastClickLocation: UIEventSource<{ lat: number, lon: number }> = new UIEventSource<{ lat: number, lon: number }>(undefined) | ||||
|     private  _previousLayer: L.tileLayer = undefined; | ||||
|     private _previousLayer: L.tileLayer = undefined; | ||||
|     public readonly CurrentLayer: UIEventSource<{ | ||||
|         id: string, | ||||
|         name: string, | ||||
|         layer: L.tileLayer | ||||
|     }> = new UIEventSource<L.tileLayer>(BaseLayers.defaultLayer); | ||||
|     }> = new UIEventSource<L.tileLayer>(Basemap.defaultLayer); | ||||
| 
 | ||||
| 
 | ||||
|     constructor(leafletElementId: string, | ||||
|                 location: UIEventSource<{ zoom: number, lat: number, lon: number }>, | ||||
|                 extraAttribution: UIElement) { | ||||
|         this._previousLayer = Basemap.defaultLayer.layer; | ||||
|         this.map = L.map(leafletElementId, { | ||||
|             center: [location.data.lat ?? 0, location.data.lon ?? 0], | ||||
|             zoom: location.data.zoom ?? 2, | ||||
|             layers: [BaseLayers.defaultLayer.layer], | ||||
|             layers: [this._previousLayer], | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|  | @ -118,9 +116,43 @@ export class Basemap { | |||
| 
 | ||||
|         this.map.on("contextmenu", function (e) { | ||||
|             self.LastClickLocation.setData({lat: e.latlng.lat, lon: e.latlng.lng}); | ||||
|             console.log("Right click") | ||||
|             e.preventDefault(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public static CreateBackgroundLayer(id: string, name: string, url: string, attribution: string, | ||||
|                                         maxZoom: number, isWms: boolean, isWMTS?: boolean) { | ||||
| 
 | ||||
|         url = url.replace("{zoom}", "{z}") | ||||
|             .replace("{switch:", "{") | ||||
|             .replace("{proj}", "EPSG:3857") | ||||
|             .replace("{width}", "256") | ||||
|             .replace("{height}", "256") | ||||
| 
 | ||||
|         //geoservices.informatievlaanderen.be/raadpleegdiensten/dhmv/wms?FORMAT=image/jpeg&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetMap&LAYERS=DHMV_II_SVF_25cm&STYLES=&SRS=EPSG:3857&WIDTH=256&HEIGHT=256
 | ||||
|         if (isWms) { | ||||
|             return { | ||||
|                 id: id, | ||||
|                 name: name, | ||||
|                 layer: L.tileLayer.wms(url, | ||||
|                     { | ||||
|                         maxZoom: maxZoom ?? 19, | ||||
|                         attribution: attribution + " | " | ||||
|                     }) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             id: id, | ||||
|             name: name, | ||||
|             layer: L.tileLayer(url, | ||||
|                 { | ||||
|                     attribution: attribution, | ||||
|                     maxZoom: maxZoom, | ||||
|                     minZoom: 1, | ||||
|                     wmts: isWMTS ?? false | ||||
|                 }) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -62,7 +62,7 @@ export class UIEventSource<T>{ | |||
|             newSource.setData(f(self.data)); | ||||
|         } | ||||
| 
 | ||||
|         this.addCallback(update); | ||||
|         this.addCallbackAndRun(update); | ||||
|         for (const extraSource of extraSources) { | ||||
|             extraSource?.addCallback(update); | ||||
|         } | ||||
|  |  | |||
							
								
								
									
										50
									
								
								UI/BackgroundSelector.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								UI/BackgroundSelector.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | |||
| import {UIElement} from "./UIElement"; | ||||
| import AvailableBaseLayers from "../Logic/AvailableBaseLayers"; | ||||
| import {DropDown} from "./Input/DropDown"; | ||||
| import Translations from "./i18n/Translations"; | ||||
| import {State} from "../State"; | ||||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| 
 | ||||
| export default class BackgroundSelector extends UIElement { | ||||
| 
 | ||||
|     private _dropdown: UIElement; | ||||
|     private readonly state: State; | ||||
|     private readonly _availableLayers: UIEventSource<any>; | ||||
| 
 | ||||
|     constructor(state: State) { | ||||
|         super(); | ||||
|         this.state = state; | ||||
| 
 | ||||
|         this._availableLayers = new AvailableBaseLayers(state).availableEditorLayers; | ||||
|         const self = this; | ||||
|         this._availableLayers.addCallbackAndRun(available => self.CreateDropDown(available)); | ||||
|     } | ||||
| 
 | ||||
|     private CreateDropDown(available) { | ||||
|         if(available.length === 0){ | ||||
|             console.warn("NO AVAILABLE LAYERS") | ||||
|         } | ||||
|          | ||||
|         console.log("ALL LAYERS", available) | ||||
|          | ||||
|         const baseLayers: { value: any, shown: string }[] = []; | ||||
|         for (const i in available) { | ||||
|             const layer: { url: string, max_zoom: number, license_url: number, name: string, geometry: any, leafletLayer: any } = available[i]; | ||||
| 
 | ||||
|             if (layer.name === undefined) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             baseLayers.push({value: layer.leafletLayer, shown: layer.name}); | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         const dropdown = new DropDown(Translations.t.general.backgroundMap, baseLayers, State.state.bm.CurrentLayer) | ||||
|         this._dropdown = dropdown; | ||||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         return this._dropdown.Render(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -73,12 +73,10 @@ export class TextField extends InputElement<string> { | |||
|     } | ||||
|      | ||||
|     Update() { | ||||
|         console.log("Updating TF") | ||||
|         super.Update(); | ||||
|     } | ||||
| 
 | ||||
|     InnerUpdate() { | ||||
|         console.log("Inner Updating TF") | ||||
|         const field = document.getElementById("txt-" + this.id); | ||||
|         const self = this; | ||||
|         field.oninput = () => { | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| import {UIElement} from "./UIElement"; | ||||
| import {CheckBox} from "./Input/CheckBox"; | ||||
| import Combine from "./Base/Combine"; | ||||
| import {Img} from "./Img"; | ||||
| import {State} from "../State"; | ||||
| import Translations from "./i18n/Translations"; | ||||
| import {FixedUiElement} from "./Base/FixedUiElement"; | ||||
|  |  | |||
|  | @ -1,5 +1,9 @@ | |||
| #! /bin/bash | ||||
| 
 | ||||
| cd assets/ | ||||
| wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json | ||||
| cd .. | ||||
| 
 | ||||
| mkdir -p assets/generated | ||||
| ts-node createLayouts.ts || { echo 'Creating layouts failed' ; exit 1; } | ||||
| find -name '*.png' | parallel optipng '{}' | ||||
|  |  | |||
							
								
								
									
										9
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										9
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -1,8 +1,5 @@ | |||
| import ValidatedTextField from "./UI/Input/ValidatedTextField"; | ||||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; | ||||
| import AvailableBaseLayers from "./Logic/AvailableBaseLayers"; | ||||
| 
 | ||||
| 
 | ||||
| const vtf= ValidatedTextField.KeyInput(true); | ||||
| vtf.AttachTo('maindiv') | ||||
| vtf.GetValue().addCallback(console.log) | ||||
| new VariableUiElement(vtf.GetValue().map(n => ""+n)).AttachTo("extradiv") | ||||
| const layers = AvailableBaseLayers.AvailableLayersAt(51.2,3.2); | ||||
| console.log(layers); | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue