Leaflet – Displaying Popups for Overlapping Polygons in the Same Layer in Leaflet

geojsonleafletpopup

I am working on a leaflet map that shows property ownership over time with polygons and a time slider (https://github.com/jeffjb4488/GeoJASON_test). However, some of my polygons overlap and only the popup for the topmost layer appears.

I came across a solution for a popup that shows tabs for multiple geojson layers (http://www.gistechsolutions.com/leaflet/DEMO/pointsinpoly/indextab.html).

I am wondering if there was a way to make this work to display tabs for multiple polygons in the same geojson layer?

Code snippet:

<body>
<div id="map"></div>
<script id="rendered-js" >
    var basemap_color = 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png';
    var basemap_satellite = 'https://api.maptiler.com/tiles/satellite-v2/{z}/{x}/{y}.jpg?key=kUPH9bhggOn8sGeOtVCW';
    var overlay_1890 = 'https://map-tile.server.burne138.msu.domains/Plan_Map_of_the_Highlands_1890_best_imagery_4326_largest/{z}/{x}/{y}.png';
    
    
    var map = L.map('map', {
        editable: true,
        minZoom: 13,
        maxZoom: 20,
        zoomControl: false}).setView([41.461070584121636, -70.56516015402862], 16);
    
    var basemapLayerColor = L.tileLayer(basemap_color, {
      noWrap: true,
      minZoom: 13,
      maxZoom: 20,
      attribution: '<div id="content-attribution">&copy; <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> </div>'
    }).addTo(map);
    
    var basemapLayerSatellite = L.tileLayer(basemap_satellite,{
            noWrap: true,
            tileSize: 512,
            zoomOffset: -1,
            maxZoom: 20,
            minZoom: 13,
            attribution: '<div id="content-attribution">&copy; <a href="https://www.maptiler.com/" target="_blank">MapTiler</a> contributors JJB |</div>',
            crossOrigin: true
          }).addTo(map);
    
    var overlayLayer1890 = L.tileLayer(overlay_1890, {
      noWrap: true,
      maxZoom: 20,
        minZoom: 13,
      opacity: 0.6,
      attribution: '<div id="content-attribution">Historic Map, <a href="https://collections.leventhalmap.org/search/commonwealth:x059cd109" target="_blank">Boston Public Library Norman B. Leventhal Map Center</div>',
    }).addTo(map);
    
    var baseMaps = {
        "Satellite": basemapLayerSatellite,
        "Standard": basemapLayerColor
       };
    
    var overlayMaps = {
        "Historic Map": overlayLayer1890
    };
    
    var mql = window.matchMedia('(max-width:991px)');
    
    let mobileView = mql.matches;    
    
    var mobileZoomBox =  L.Control.extend({
    
    options: {
      position: 'topright'
    },
     
    onAdd: function (map) {
      var container = L.DomUtil.create('div');
      container.type="div";
      container.title="Mobile Zoom Box";
      container.textContent = 'Pinch To Zoom';
      
      container.style.backgroundColor = 'white';
      container.style.fontSize = "22px";
      container.style.fontWeight = "700";
      container.style.textAlign = "center";
      container.style.backgroundSize = "100px 65px";
      container.style.width = '100px';
      container.style.height = '65px';
      
    
      return container;
    }
    });
    
    if (mobileView) {
        map.addControl(new mobileZoomBox());;
    } else {
        var zoom_bar = new L.Control.ZoomBar({position: 'topright'}).addTo(map);
    }
    
    
    L.control.layers(baseMaps, overlayMaps).addTo(map);
    
    var infoDesktop = L.control();
    var infoMobile = L.control();
    
    infoDesktop.onAdd = function (map) {
        this._div = L.DomUtil.create('div', 'info'); // create a div with a class "info"
        this.update();
        return this._div;
    };
    
    infoMobile.onAdd = function (map) {
        this._div = L.DomUtil.create('div', 'info'); // create a div with a class "info"
        this.update();
        return this._div;
    };
    
    // method that we will use to update the control based on feature properties passed
    infoDesktop.update = function (props) {
            this._div.innerHTML = '<div id="content-desktop"><h3>Lot Information</h3>' +  (props ?
            '<b>' + 'Lot #' + props.lot_number + ' owned by ' + props.grantee + ' starting on ' + props.time
            : 'Hover over a lot</div>');
        };
        
    infoMobile.update = function (props) {
            this._div.innerHTML = '<div id="content-mobile"><h2>Lot Information</h2>' +  (props ?
            '<b>' : '<h3>Click on a lot</h3></div>');
            };
    
    if (mobileView) {
        infoMobile.addTo(map);
    } else {
        infoDesktop.addTo(map);
    }
    
    
    function style(feature) {
            return {
                weight: 2,
                opacity: 1,
                color: 'black',
                dashArray: '3',
                fillOpacity: 0.7,
                fillColor: 'green',
            };
        }
    
    function highlightFeature(e) {
        var layer = e.target;
    
        layer.setStyle({
            weight: 5,
            color: '#666',
            dashArray: '',
            fillOpacity: 0.7
        });
    
        if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
            layer.bringToFront();
        };
    
        infoDesktop.update(layer.feature.properties);
        infoMobile.update(layer.feature.properties);
    }
    
    var polygons
    
    
    
        function resetHighlight(e) {
            polygons.resetStyle(e.target);
            infoDesktop.update();
            infoMobile.update();
        }
    
        function zoomToFeature(e) {
            map.fitBounds(e.target.getBounds());
        }
        
      function onEachFeature (feature, layer) {
        layer.on({
            mouseover: highlightFeature,
            mouseout: resetHighlight,
            dblclick: zoomToFeature,
        });
    
      
    
        
        var content = "<div style='clear: both'></div><div><center><img src='images/" + feature.properties.picture + "' style='width:200px;height:300x;'></center> <br><h4>" + 'Lot number ' + feature.properties.lot_number + "</h4><p>" + feature.properties.time + "</p><p>" + feature.properties.grantee + "</p><a href='" + feature.properties.link + "' target='_blank'>" + "Click here for more info" + "</a>" + "</p></div>";
        layer.bindPopup(content, {closeButton: true});
        var p = layer.feature.properties;
        p.index = "Lot " + p.lot_number + " | " + p.grantee + " | " + p.time;
        };
    
    
        $.getJSON("data/test_polygon_wgs84_5.geojson", function (json){
    // add GeoJSON layer to the map once the file is loaded
       polygons = L.geoJson(json ,{
            style: style,
            onEachFeature: onEachFeature,
            });map.fitBounds(polygons.getBounds());
    
    // Set the sliderControl options
        var sliderControl = L.control.sliderControl({
            layer: polygons, //REQUIRED
            alwaysShowDate: true,
            range: true,
            sameDate: true,
            showAllPopups: false, // to show all popups, instead of one, same as popup option "autoClose: false"
            showPopups: false, // to set it so, when a marker is generated by the slider, its popup doesn't automatically display
            showAllOnStart: true        
        });
    
    
    
        var searchControl = new L.Control.Search({
            layer: polygons,
            propertyName: 'index',
            marker: false,
            moveToLocation: function(latlng, title, map) {
                //map.fitBounds( latlng.layer.getBounds() );
                var zoom = map.getBoundsZoom(latlng.layer.getBounds());
                map.setView(latlng, zoom); // access the zoom
            }
    });        
    
    searchControl.on('search:locationfound', function(e) {
            
            //console.log('search:locationfound', );
    
            //map.removeLayer(this._markerSearch)
    
            e.layer.setStyle({fillColor: '#3f0', color: '#0f0'});
            if(e.layer._popup)
                e.layer.openPopup();
    
        }).on('search:collapsed', function(e) {
    
            polygons.eachLayer(function(layer) {    //restore feature color
                polygons.resetStyle(layer);
            }); 
    });
        
    map.addControl( searchControl );  //inizialize search control
        
    // Add the slider to the map
    map.addControl(sliderControl);
    
    // Initialize the slider
    sliderControl.startSlider();
    
    });
    
    var sidebar = L.control.sidebar({ container: 'sidebar' })
                .addTo(map)
                .open('home');
    
    
    
    
    </script>
    
      
    
    </body>

Best Answer

On way to do it is to use your click processing function zoomToFeature and there with the help of turf.js library function turf.booleanPointInPolygon check which features contain clicked point coordinate and then build and open your popup there, instead of defining popup in onEachFeature function.

Relevant part of the code could then look something like this:

function zoomToFeature(e) {
  map.fitBounds(e.target.getBounds());
  var point = turf.point([e.latlng.lng, e.latlng.lat]);
  var content = '';
  polygons.eachLayer(function(layer) {
    var feature = layer.feature;
    if (turf.booleanPointInPolygon(point, feature)) {
      content += "<div style='clear: both'></div><div><center><img src='images/" + feature.properties.picture + "' style='width:200px;height:300x;'></center> <br><h4>" + 'Lot number ' + feature.properties.lot_number + "</h4><p>" + feature.properties.time + "</p><p>" + feature.properties.grantee + "</p><a href='" + feature.properties.link + "' target='_blank'>" + "Click here for more info" + "</a>" + "</p></div>";
    }
  });
  L.popup()
    .setLatLng(e.latlng)
    .setContent(content)
    .openOn(map);
}

function onEachFeature (feature, layer) {
  layer.on({
    mouseover: highlightFeature,
    mouseout: resetHighlight,
    click: zoomToFeature
  });
}