Search code examples
leafletopacityblending

Leaflet: Blend between two basemap-layers


I want to create a Leaflet map, which is able to switch between different map layers. This is of course no problem.

But I want to add a slider where I can blend between the actual maplayer and the former maplayer to conveniently compare the content of these 2 maps:
slider = right => 100% opacity of actual map
slider = left => 0% opacity of actual map, which means 100% opacity of former map
slider = middle => 50% opacity of actual map, which means former map in the background shines through by also 50%

I managed to get the blend working in the initial situation after loading the map. But I can't get it working after I choose another map from the map selector menu. The former maplayer which should be displayed in the background seems to get lost.

I thing that the problem is that the function "fct_layerchange" is not only called by the event-listenier map.on when I choose another map within the Maplayers menu, but also when I add the former maplayer to the background by "map.addLayer(bgMap);"

I have no idea any more how to solve these unwanted "multicall" of the function, since I rely on the 'baselayerchange'-event when choosing a new basemap. And this event is fired again when being inside the event's function Does some Leaflet-guru has an idea how I could solve this? :-)

<!DOCTYPE html> 
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blend 2 maps</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<style>
    html, body, #map {height: 100%;}
</style>
</head>

<body>
<div id="map"></div>
<script>

// 1.) BASEMAPS
var osm_mapnik = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.openstreetmap.org">Openstreetmap</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });
var thunder_cycle = L.tileLayer('https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png', {subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.thunderforest.com">Thunderforest</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });
var thunder_outdoors = L.tileLayer('https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png', {subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.thunderforest.com">Thunderforest</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });
var thunder_pioneer = L.tileLayer('https://{s}.tile.thunderforest.com/pioneer/{z}/{x}/{y}.png', {subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.thunderforest.com">Thunderforest</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });

// 2.) OVERLAYMAPS
var heidel_bound = L.tileLayer('http://korona.geog.uni-heidelberg.de/tiles/adminb/x={x}&y={y}&z={z}', {maxNativeZoom: 18, maxZoom:19, noWrap:true, attribution:'<a href="http://korona.geog.uni-heidelberg.de/contact.html">Uni-Heidelberg</a>' });
var wmt_hiking = L.tileLayer('https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png', {maxNativeZoom: 18, maxZoom:19, noWrap:true, attribution:'<a href="http://waymarkedtrails.org">Waymarkedtrails</a>' });
 
// LAYERMENU
var baseMaps = {
    "OpenStreetMap Mapnik": osm_mapnik,
    "Thunderforest Opencycle": thunder_cycle,
    "Thunderforest Outdoors": thunder_outdoors,
    "Thunderforest Pioneer": thunder_pioneer
};

var map = L.map ( 'map', { center: [47, 15], zoom: 11, layers: [thunder_cycle, wmt_hiking] } );
var overlayMaps = {
    "Hiking Routes": wmt_hiking,
    "Boundaries": heidel_bound,
};
var ctr_mapLayers = L.control.layers(baseMaps, overlayMaps).addTo(map);
var fgMap = thunder_cycle;
var bgMap = thunder_pioneer;
map.addLayer(bgMap);            // add initial backgroundmap-layer to map
bgMap.bringToBack();            // move backgroundmap-Layer to to the background of the map

function fct_blend() {
  valBlend = document.getElementById("id_sliderBlend").value;
  document.getElementById("id_valBlend").innerHTML = Number(valBlend).toFixed(1)
  fgMap.setOpacity(valBlend);
}
 
var ctr_blend = L.control();
ctr_blend.onAdd = function (map) {
    valOpacity = 1.0;
    this.div = L.DomUtil.create('div');
    this.div.innerHTML = '<span id="id_valBlend">1.0</span><input type="range" id="id_sliderBlend" min="0" max="1" step="0.1" value="1" style="width:100px;" oninput="fct_blend()">';
    L.DomEvent.disableClickPropagation(this.div);
    return this.div;
};
ctr_blend.addTo(map);

var fct_layerchange = function (e) {
    bgMap = fgMap;
    bgMap.setOpacity(1);         // set opacity of former foregroundmap-layer which is now background-layer to 1.0;
    map.addLayer(bgMap);         // add former foregroundmap-layer as backgroundmap-layer to map again. 
    fgMap = e.layer;             // update fgMap-variable with the actual foregroundmap-layer
    fgMap.setOpacity(valBlend);  // set opacity of the new foregroundmap-layer to the actual blend-Value.
};

map.on('baselayerchange', fct_layerchange);

</script>
</body>
</html>


Solution

  • I want to inform you, that in the end I could found a working solution. After hours and hours of trial and error and researching the source code of Leaflet I found out what has been the problem and how to solve it.

    The problem in my case was that Leaflet's layer control object is strictly managing each of its basemaps and overlaymaps (= only one basemaplayer can be displayed the same time) If I would add another baselayer for the use in the background, the layer control object will immidiately remove it again, even if I've put it on another pane.

    Workaround is: create an indivdual copy of each basemap (for example "osm_mapnik" will get a twin called "osm_mapnik_bg") for the use on the background pane. The reason: layer control object has just control of "osm_mapnik", but I have the full control over "osm_mapnik_bg" :-)

    So the layer control manages the changes of the forderground map and I manage the change of the background map - perfect division of labour.

    <!DOCTYPE html> 
    <html>
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Blend 2 maps</title>
    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
    <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
    <style>
        html, body, #map { height: 100%; }
        html, body, #map { margin: 0; padding: 0; }
    </style>
    </head>
    
    <body>
    <div id="map"></div>
    <script>
    
    // 1.) BASEMAPS
    var osm_mapnik = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.openstreetmap.org">Openstreetmap</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });
    var thunder_cycle = L.tileLayer('https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png', {subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.thunderforest.com">Thunderforest</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });
    var thunder_outdoors = L.tileLayer('https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png', {subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.thunderforest.com">Thunderforest</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });
    var thunder_pioneer = L.tileLayer('https://{s}.tile.thunderforest.com/pioneer/{z}/{x}/{y}.png', {subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.thunderforest.com">Thunderforest</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });
    
    // 1b.) BASEMAPS copies for use on background pane: use 'mapPane' which's z-index is lower (=behind) the 'tilePane' used by the layers in 1.)
    var osm_mapnik_bg = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {pane:'mapPane', subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.openstreetmap.org">Openstreetmap</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });
    var thunder_cycle_bg = L.tileLayer('https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png', {pane:'mapPane', subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.thunderforest.com">Thunderforest</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });
    var thunder_outdoors_bg = L.tileLayer('https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png', {pane:'mapPane', subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.thunderforest.com">Thunderforest</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });
    var thunder_pioneer_bg = L.tileLayer('https://{s}.tile.thunderforest.com/pioneer/{z}/{x}/{y}.png', {pane:'mapPane', subdomains:'abc', maxZoom:19, noWrap:true, attribution:'<a href="http://www.thunderforest.com">Thunderforest</a> | <a href="http://www.openstreetmap.org/copyright/">OpenStreetMap</a>' });
    
    // 2.) OVERLAYMAPS
    var heidel_bound = L.tileLayer('http://korona.geog.uni-heidelberg.de/tiles/adminb/x={x}&y={y}&z={z}', {maxNativeZoom: 18, maxZoom:19, noWrap:true, attribution:'<a href="http://korona.geog.uni-heidelberg.de/contact.html">Uni-Heidelberg</a>' });
    var wmt_hiking = L.tileLayer('https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png', {maxNativeZoom: 18, maxZoom:19, noWrap:true, attribution:'<a href="http://waymarkedtrails.org">Waymarkedtrails</a>' });
     
    // LAYERMENU
    var baseMaps = {
        "OpenStreetMap Mapnik": osm_mapnik,
        "Thunderforest Opencycle": thunder_cycle,
        "Thunderforest Outdoors": thunder_outdoors,
        "Thunderforest Pioneer": thunder_pioneer
    };
    
    // needed to get the layer's Objectname by its Layer-Controlname which is the only name passed by the 'baselayerchange'-event  
    var layerLookup = {"OpenStreetMap Mapnik":"osm_mapnik", "Thunderforest Opencycle":"thunder_cycle", "Thunderforest Outdoors":"thunder_outdoors", "Thunderforest Pioneer":"thunder_pioneer",};
    
    var map = L.map ( 'map', { center: [47, 15], zoom: 11, layers: [thunder_cycle] } );
    var overlayMaps = {
        "Hiking Routes": wmt_hiking,
        "Boundaries": heidel_bound,
    };
    var ctr_mapLayers = L.control.layers(baseMaps, overlayMaps).addTo(map);
    
    var fgLayerControlname = "Thunderforest Opencycle";       // default foreground-Layer*Controlname*
    var fgLayer = window [layerLookup [fgLayerControlname]];  // default foreground-Layer Object
    var bgLayerName = 'thunder_pioneer_bg';                   // default background-Layer*Objectname*
    var bgLayer = thunder_pioneer_bg;                         // default background-Layer Object
    map.addLayer(bgLayer);        
    
    function fct_blend() {
      valBlend = document.getElementById("id_sliderBlend").value;
      document.getElementById("id_valBlend").innerHTML = Number(valBlend).toFixed(1)
      fgLayer.setOpacity(valBlend);
    }
     
    var ctr_blend = L.control({position:'bottomright'});
    ctr_blend.onAdd = function (map) {
        valBlend = 1.0;
        this.div = L.DomUtil.create('div');
        this.div.innerHTML = '<span id="id_valBlend">1.0</span><input type="range" id="id_sliderBlend" min="0" max="1" step="0.1" value="1" style="width:100px;" oninput="fct_blend()">';
        L.DomEvent.disableClickPropagation(this.div);
        return this.div;
    };
    ctr_blend.addTo(map);
    
    var fct_layerchange = function (e) {
        map.removeLayer(bgLayer);                                   // remove former bg-Layer
        bgLayerName = [layerLookup [fgLayerControlname]] + '_bg';   // set Object*name* of new bg-Layer which is former fg-Layer
        bgLayer = window[bgLayerName];                              // set bgLayer-Object out of its Object*name* 
        map.addLayer(bgLayer);                                      // add former foregroundmap-layer as backgroundmap-layer to map again. 
        fgLayerControlname = e.name;                                // get fg-Layer *Controlname* which is used in the Layercontrol-menu (its Objectname is not passed by the event)
        fgLayer = window [layerLookup [fgLayerControlname]];        //set fgLayer-Object by the use of its Objectname which is derived from its *Controlname*
        fgLayer.setOpacity(valBlend);  // set opacity of the new foreground-layer to the actual blend-Value.
    };
    
    map.on('baselayerchange', fct_layerchange);                     // fired if a new maplayer is choosen by the Layercontrol
    
    </script>
    </body>
    </html>