Leaflet – Appending Label to Every Layer in Current Map Bounds

leafletleaflet-plugins

I am trying to reduce the amount of permanent tooltip on my leaflet map for performance reasons. I have over 5000 geoJSON Polygons in my non-geographic map. At a specific zoom level, my Polygons should receive a label a.k.a Tooltip.

So my idea is to check each layer if it is in the current map bounds. But getting bounds and center LatLngs of over 5000 layers and checking if that layer is in the map bounds is time-consuming. If I add this in the map.moveend Event my map freeze. In addition, I have to fetch text for my labels from an SQLite Database.

Is there another solution for this problem?

EDIT:

My code so far:

var allLayer = L.geoJson(xhr.response, {style: style}).addTo(mymap);

var layerInCurrrentMapBounds = [];
mymap.on('moveend', function(e) {
    if (mymap.getZoom() >= 5) {
        allLayer.eachLayer(function(layer) {
            var bounds = layer.getBounds();
            if (bounds.isValid()) {
                var latLng = bounds.getCenter();
                if (mymap.getBounds().contains(latLng)) {
                    layerInCurrrentMapBounds.push(layer);
                }
            }
        });

        layerInCurrrentMapBounds.eachLayer(function(layer) {
            layer.bindTooltip("test", {
                permanent: true,
                className: "my-label",
                offset: [0, 0]
            });
        });
    }
});

On the next moveend I have to remove all tooltips. In addition, I have to fetch my tooltip text from a sqlite database. And what if the user starts to drag the map rapidly. Could I make my function async and cancel this every time the user starts to drag again?

Best Answer

Solution 1 (see optimized solution 2 below after this one):

On possible solution would be to use very efficient Turf.js library for geospatial operations when checking if feature is visible in current view.

Example below creates 5000 GeoJSON polygons and displays them on the map, with permanent tooltips for polygons that are visible when zoom level is 12 or more. When map is panned or zoomed, tooltips are created for newly shown polygons and removed for those that are no more visible. Working JSFiddle is available here: https://jsfiddle.net/TomazicM/k4erLnf1/

When creating random polygons each on is given unique id. Then id is then used when creating layersById object which serves as a link between Leaflet layers/features and corresponding GeoJSON features/polygons when analyzing which GeoJSON polygons are visible in the current map view.

This is the code:

var map = L.map('map');

L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  maxZoom: 18,
  attribution: '&copy; <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

var polygons;
var layersById = {};
var layersWithTooltip = {};

function onEachFeature(feature, layer) {
  layersById[feature.id] = layer;
}

setTimeout(function() {
  polygons = turf.randomPolygon(5000, {bbox: [13.24, 45.5, 16.35, 46.84], max_radial_length: 0.01 });
  turf.featureEach(polygons, function (currentFeature, featureIndex) {
    currentFeature.id = featureIndex;
  });
  var geojson = L.geoJson(polygons, {
    onEachFeature: onEachFeature
  }).addTo(map);

  map.setView(geojson.getBounds().getCenter(), 13);
  updateTooltips();
  document.getElementById('loading').style.display = 'none';
}, 300);

map.setView([37.8, -96], 12);

function updateTooltips() {
  if (map.getZoom() < 12) {
    for (id in layersWithTooltip) {
      layersWithTooltip[id].unbindTooltip();
    }
    layersWithTooltip = {};
    return;
  }

  var bounds = map.getBounds();     
  var bboxPoly = turf.bboxPolygon([bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]);

  var oldLayersWithTooltip = layersWithTooltip;
  layersWithTooltip = {};

  turf.featureEach(polygons, function (currentFeature, featureIndex) {
    if (turf.booleanContains(bboxPoly, turf.centerOfMass(currentFeature))) {
      var layer = layersById[currentFeature.id];
      layersWithTooltip[currentFeature.id] = layer;
      if ((typeof layer.getTooltip() == 'undefined') || (layer.getTooltip() == null)) {
        layer.bindTooltip('' + currentFeature.id, {permanent: true});
        }
      else {
        delete oldLayersWithTooltip[currentFeature.id];
      }
    }
  });

  for (id in oldLayersWithTooltip) {
    oldLayersWithTooltip[id].unbindTooltip();
  }
}

var startTime;
var endTime;

map.on('movestart', function(evt) {
  startTime = new Date().getTime();
});

map.on('moveend', function(evt) {
  updateTooltips();
  endTime = new Date().getTime();
  console.log('elapsed: ' + (endTime - startTime));
});

Solution 2 (much quicker): Based on solution 1 as far as Turf.js library is concerned, this solution is using Leaflet.VectorGrid plugin for much quicker rendering of big GeoJSON layer. Since plugin does not support adding tooltips to features/layers, dummy (empty) L.divIcon markers are created as placeholders for tooltips. Working JSFiddle is available here: https://jsfiddle.net/TomazicM/g5pwd3mk/

This is the main part of the code:

<style>
  .emptyIcon {
  }
</style>
.
.
.
var map = L.map('map').setView([46, 14.84], 12);

L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  maxZoom: 18,
  attribution: '&copy; <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

var polygons = turf.randomPolygon(5000, {bbox: [13.24, 45.5, 16.35, 46.84], max_radial_length: 0.01 })

turf.featureEach(polygons, function (currentFeature, featureIndex) {
  currentFeature.id = featureIndex;
});

var vectorGrid = L.vectorGrid.slicer(polygons, {
  maxZoom: 18,
  rendererFactory: L.svg.tile,
  vectorTileLayerStyles: {
    sliced: function(properties, zoom) {
      return {
        fillColor: '#E31A1C',
        fillOpacity: 0.5,
        stroke: true,
        fill: true,
        color: 'blue',
        weight: 1
      }
    }
  },
  interactive: false,
  getFeatureId: function(f) {
    return f.id;
  }
}).addTo(map);

var markerLayer = L.layerGroup([]).addTo(map);
var markersWithTooltip = {};

function updateTooltips() {
  if (map.getZoom() < 12) {
    markerLayer.clearLayers();
    markersWithTooltip = {};
    return;
  }

  var bounds = map.getBounds();     
  var bboxPoly = turf.bboxPolygon([bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]);

  var oldmarkersWithTooltip = markersWithTooltip;
  markersWithTooltip = {};

  turf.featureEach(polygons, function (currentFeature, featureIndex) {
    var featureCenter = turf.centerOfMass(currentFeature);
    if (turf.booleanContains(bboxPoly, featureCenter)) {
      if (typeof (oldmarkersWithTooltip[currentFeature.id]) == 'undefined') {
        var emptyIcon = L.divIcon({html: '', className: 'emptyIcon'});
        var marker = L.marker([featureCenter.geometry.coordinates[1], featureCenter.geometry.coordinates[0]], {icon: emptyIcon, interactive: false}).addTo(map);
        marker.bindTooltip('' + currentFeature.id, {permanent: true});
        markersWithTooltip[currentFeature.id] = marker;
        }
      else {
        delete oldmarkersWithTooltip[currentFeature.id];
      }
      if ((typeof (markersWithTooltip[currentFeature.id]) != 'undefined') && !markerLayer.hasLayer(markersWithTooltip[currentFeature.id])) {
        markersWithTooltip[currentFeature.id].addTo(markerLayer);
      }
    }
  });

  for (id in oldmarkersWithTooltip) {
    markerLayer.removeLayer(oldmarkersWithTooltip[id]);
  }
}

updateTooltips();

var startTime;
var endTime;


map.on('movestart', function(evt) {
  startTime = new Date().getTime();
});

map.on('moveend', function(evt) {
  updateTooltips();
  endTime = new Date().getTime();
  console.log('elapsed: ' + (endTime - startTime));
});