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: '© <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: '© <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));
});