diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index e250a329..157f32bf 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -2,7 +2,10 @@ L.U.BaseAction = L.ToolbarAction.extend({ initialize: function (map) { this.map = map if (this.options.label) { - this.options.tooltip = this.map.help.displayLabel(this.options.label, withKbdTag=false) + this.options.tooltip = this.map.help.displayLabel( + this.options.label, + (withKbdTag = false) + ) } this.options.toolbarIcon = { className: this.options.className, @@ -18,7 +21,7 @@ L.U.ImportAction = L.U.BaseAction.extend({ options: { helpMenu: true, className: 'upload-data dark', - label: 'IMPORT_PANEL' + label: 'IMPORT_PANEL', }, addHooks: function () { @@ -87,7 +90,7 @@ L.U.DrawMarkerAction = L.U.BaseAction.extend({ options: { helpMenu: true, className: 'umap-draw-marker dark', - label: 'DRAW_MARKER' + label: 'DRAW_MARKER', }, addHooks: function () { @@ -99,7 +102,7 @@ L.U.DrawPolylineAction = L.U.BaseAction.extend({ options: { helpMenu: true, className: 'umap-draw-polyline dark', - label: 'DRAW_LINE' + label: 'DRAW_LINE', }, addHooks: function () { @@ -111,7 +114,7 @@ L.U.DrawPolygonAction = L.U.BaseAction.extend({ options: { helpMenu: true, className: 'umap-draw-polygon dark', - label: 'DRAW_POLYGON' + label: 'DRAW_POLYGON', }, addHooks: function () { @@ -1184,21 +1187,30 @@ L.U.Map.include({ /* Used in view mode to define the current tilelayer */ L.U.TileLayerControl = L.Control.IconLayers.extend({ initialize: function (map, options) { - const layers = [] - map.eachTileLayer((layer) => { - layers.push({ - title: layer.options.name, - layer: layer, - icon: L.Util.template(layer.options.url_template, map.demoTileInfos), - }) - }) - const maxShown = 10 - L.Control.IconLayers.prototype.initialize.call(this, layers.slice(0, maxShown), { + this.map = map + L.Control.IconLayers.prototype.initialize.call(this, { position: 'topleft', - manageLayers: false + manageLayers: false, }) this.on('activelayerchange', (e) => map.selectTileLayer(e.layer)) }, + + setLayers: function (layers) { + if (!layers) { + layers = [] + this.map.eachTileLayer((layer) => { + layers.push({ + title: layer.options.name, + layer: layer, + icon: L.Util.template(layer.options.url_template, this.map.demoTileInfos), + }) + }) + } + const maxShown = 10 + L.Control.IconLayers.prototype.setLayers.call(this, layers.slice(0, maxShown)) + if (this.map.selected_tilelayer) this.setActiveLayer(this.map.selected_tilelayer) + }, + }) /* Used in edit mode to define the default tilelayer */ @@ -1261,6 +1273,7 @@ L.U.TileLayerChooser = L.Control.extend({ 'click', function () { this.map.selectTileLayer(tilelayer) + this.map._controls.tilelayers.setLayers() if (options && options.callback) options.callback(tilelayer) }, this diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 39addb50..c814854e 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -166,7 +166,7 @@ L.U.Map.include({ this.help = new L.U.Help(this) if (this.options.hash) this.addHash() - this.initTileLayers(this.options.tilelayers) + this.initTileLayers() // Needs tilelayer to exist for minimap this.initControls() // Needs locate control and hash to exist @@ -348,6 +348,7 @@ L.U.Map.include({ this.importer = new L.U.Importer(this) this.drop = new L.U.DropControl(this) this._controls.tilelayers = new L.U.TileLayerControl(this) + this._controls.tilelayers.setLayers() this.renderControls() }, @@ -606,6 +607,7 @@ L.U.Map.include({ } else { this.selectTileLayer(this.tilelayers[0]) } + if (this._controls) this._controls.tilelayers.setLayers() }, createTileLayer: function (tilelayer) { @@ -649,8 +651,18 @@ L.U.Map.include({ }, eachTileLayer: function (callback, context) { - if (this.customTilelayer) callback.call(context, this.customTilelayer) - this.tilelayers.forEach((layer) => callback.call(context, layer)) + const urls = [] + const callOne = (layer) => { + // Prevent adding a duplicate background, + // while adding selected/custom on top of the list + const url = layer.options.url_template + if (urls.indexOf(url) !== -1) return + callback.call(context, layer) + urls.push(url) + } + if (this.selected_tilelayer) callOne(this.selected_tilelayer) + if (this.customTilelayer) callOne(this.customTilelayer) + this.tilelayers.forEach(callOne) }, setOverlay: function () { diff --git a/umap/tests/integration/test_tilelayer.py b/umap/tests/integration/test_tilelayer.py new file mode 100644 index 00000000..b2922b6f --- /dev/null +++ b/umap/tests/integration/test_tilelayer.py @@ -0,0 +1,114 @@ +import re + +import pytest +from playwright.sync_api import expect + +from umap.models import TileLayer + +from ..base import TileLayerFactory + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def tilelayers(): + # Create one more TileLayer than what we display in the switcher (11 vs 10) + TileLayerFactory( + rank=1, + name="OpenStreetMap", + url_template="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + ) + TileLayerFactory( + rank=2, + name="Forte", + url_template="https://{s}.forte.tiles.quaidorsay.fr/fr{r}/{z}/{x}/{y}.png", + ) + TileLayerFactory( + rank=3, + name="Positron", + url_template="https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png", + ) + TileLayerFactory( + rank=4, + name="Humanitarian", + url_template="//{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", + ) + TileLayerFactory( + rank=5, + name="Dark Matter", + url_template="https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png", + ) + TileLayerFactory( + rank=6, + name="OSM OpenCycleMap", + url_template="https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=e6b144cfc47a48fd928dad578eb026a6", + ) + TileLayerFactory( + rank=7, + name="CyclOSM", + url_template="//{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png", + ) + TileLayerFactory( + rank=8, + name="Piano", + url_template="https://{s}.piano.tiles.quaidorsay.fr/fr{r}/{z}/{x}/{y}.png", + ) + TileLayerFactory( + rank=9, + name="IGN Image aƩrienne (France)", + url_template="https://wxs.ign.fr/pratique/wmts/?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ORTHOIMAGERY.ORTHOPHOTOS&STYLE=normal&TILEMATRIXSET=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=image%2Fjpeg", + ) + TileLayerFactory( + rank=10, + name="OSM OpenRiverboatMap", + url_template="//{s}.tile.openstreetmap.fr/openriverboatmap/{z}/{x}/{y}.png", + ) + TileLayerFactory( + rank=11, + name="OSM OpenTopoMap", + url_template="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", + ) + + +def test_map_should_display_first_tilelayer_by_default( + map, live_server, tilelayers, page +): + page.goto(f"{live_server.url}/map/new") + tiles = page.locator(".leaflet-tile-pane img") + expect(tiles.first).to_have_attribute( + "src", re.compile(r"https://[abc].tile.openstreetmap.org/\d+/\d+/\d+.png") + ) + + +def test_map_should_display_selected_tilelayer(map, live_server, tilelayers, page): + piano = TileLayer.objects.get(name="Piano") + url_pattern = re.compile( + r"https://[abc]{1}.piano.tiles.quaidorsay.fr/fr/\d+/\d+/\d+.png" + ) + map.settings["properties"]["tilelayer"]["url_template"] = piano.url_template + map.settings["properties"]["tilelayersControl"] = True + map.save() + page.goto(f"{live_server.url}{map.get_absolute_url()}") + tiles = page.locator(".leaflet-tile-pane img") + expect(tiles.first).to_have_attribute("src", url_pattern) + iconTiles = page.locator(".leaflet-iconLayers .leaflet-iconLayers-layer") + # The second of the list should be the current + expect(iconTiles.nth(1)).to_have_css("background-image", url_pattern) + + +def test_map_should_display_custom_tilelayer(map, live_server, tilelayers, page): + # Add one not on the list + url_pattern = re.compile( + r"https://[abc]{1}.basemaps.cartocdn.com/rastertiles/voyager/\d+/\d+/\d+.png" + ) + map.settings["properties"]["tilelayer"][ + "url_template" + ] = "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png" + map.settings["properties"]["tilelayersControl"] = True + map.save() + page.goto(f"{live_server.url}{map.get_absolute_url()}") + tiles = page.locator(".leaflet-tile-pane img") + expect(tiles.first).to_have_attribute("src", url_pattern) + iconTiles = page.locator(".leaflet-iconLayers .leaflet-iconLayers-layer") + # The second of the list should be the current + expect(iconTiles.nth(1)).to_have_css("background-image", url_pattern)