2

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?

  • How long does it take to populate all 5000 features with tooltips? – TomazicM Oct 16 at 17:37
  • It takes more then 3 minutes but that's not the main problem. It's very stuttering on panning as described here: github.com/Leaflet/Leaflet/issues/6734 – R. Kut Oct 16 at 17:54
  • I was not aware of that problem. This really complicates things. – TomazicM Oct 16 at 18:08
  • I did a quick test with 5000 randomly generated polygons. Checking of visible features was done with turf.js. Without tooltips map move took around second, with tooltips it took around second and a half. Would that be an improvement over your method? – TomazicM Oct 17 at 19:59
  • Absolutely yes. I never heard of turf.js could you tell me more about your solution? – R. Kut Oct 17 at 20:10
1

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));
});
  • awesome, thank you – R. Kut Oct 19 at 15:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.