From 5d2b9688635530b6b51817f71926638ce48f79dd Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 16 Dec 2023 09:16:13 +0100 Subject: [PATCH 1/5] chore: prettier --- umap/static/umap/js/umap.controls.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index e250a329..bd753209 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 () { From cbb02f9890439bb2cf7ea805551c83c399a601cb Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 16 Dec 2023 09:17:02 +0100 Subject: [PATCH 2/5] Make sure we update the tilelayers switcher when setting a custom one --- umap/static/umap/js/umap.controls.js | 30 ++++++++++++++++++---------- umap/static/umap/js/umap.js | 4 +++- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index bd753209..523d6316 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -1187,21 +1187,28 @@ 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)) + }, }) /* Used in edit mode to define the default tilelayer */ @@ -1264,6 +1271,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..b055dbf2 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) { From 426297df4f785466312ce6486fe67177a69459b4 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 16 Dec 2023 18:47:14 +0100 Subject: [PATCH 3/5] Make sure we do not display twice the same background layer in selector At this stage, uMap does not distinguish between a custom background and the default background, both are saved in map.options.tilelayer. Given we want a custom background (so not in the list) to appear in the selector, we need this check to be sure we are not adding again one layer from the list --- umap/static/umap/js/umap.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index b055dbf2..c814854e 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -651,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 () { From 629a049eba523d7a8bba792f8332d33444044853 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 16 Dec 2023 18:50:08 +0100 Subject: [PATCH 4/5] Inform iconLayers when we change current tilelayer When an editor change the background layer from our own selector, we need to inform iconLayers, so it can update its list and order accordingly --- umap/static/umap/js/umap.controls.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 523d6316..157f32bf 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -1208,7 +1208,9 @@ L.U.TileLayerControl = L.Control.IconLayers.extend({ } 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 */ From a0279165ce27fb8e0201337129cfa074b5f3b017 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 16 Dec 2023 19:26:10 +0100 Subject: [PATCH 5/5] Add minimal integration tests for tilelayers --- umap/tests/integration/test_tilelayer.py | 114 +++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 umap/tests/integration/test_tilelayer.py 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)