1

I have a polygon layer being loaded into Leaflet in GeoJson format. When using the code below I'm able to get labels for each feature to appear at the center but they remain fixed there. I'd like for the label to remain in frame when panning/zooming around the map but I'm not having much luck on finding a way to accomplish this.

Conceptually I was thinking of using Turf.js to clip visible layers to the bounding box of the frame and recreate the labels based on that clipped feature's centerpoint.....but I'm sensing there's an easier way.

var justBoxesCoords = {
    "type": "FeatureCollection",
    "name": "Just_Boxes",
    "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
    "features": [
    { "type": "Feature", "properties": { "OID_": 0, "Shape_Leng": 64756.395995600004, "Shape_Area": 226565856.76100001, "NAME": "BOX A" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -80.774356391317028, 27.822310348006031 ], [ -80.742708836075678, 27.822255003215457 ], [ -80.742852235235631, 27.761330877183124 ], [ -80.774482152356711, 27.76138608013256 ], [ -80.774356391317028, 27.822310348006031 ] ] ] ] } },
    { "type": "Feature", "properties": { "OID_": 0, "Shape_Leng": 53766.407119000003, "Shape_Area": 160601474.13800001, "NAME": "BOX B" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -80.724177015589376, 27.80710446026918 ], [ -80.668736433230038, 27.806982625663125 ], [ -80.668811150957822, 27.782334107834661 ], [ -80.724239228912936, 27.782455816025475 ], [ -80.724177015589376, 27.80710446026918 ] ] ] ] } },
    { "type": "Feature", "properties": { "OID_": 0, "Shape_Leng": 22656.284760800001, "Shape_Area": 32081702.447799999, "NAME": "BOX C" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -80.724253304004648, 27.776874978683274 ], [ -80.706736796747734, 27.776838925234912 ], [ -80.706778562939078, 27.761259087427607 ], [ -80.724292575595669, 27.76129511721139 ], [ -80.724253304004648, 27.776874978683274 ] ] ] ] } }
    ]
    };

var justBoxes = L.geoJson(justBoxesCoords,{
    onEachFeature: function(feature,layer){
        layer.bindTooltip(feature.properties.NAME,{permanent:true,direction:'center'});
    }
}).addTo(map);

Here are some pics to show what I'm looking to do.

Polygons zoomed out Polygons zoomed in

UPDATED

For anyone that may stumble upon this post, here is what I put together using Turf.js since I don't have experience extending Leaflet and wasn't ready to jump into it yet. Probably better/smarter ways to do some of this but I'm still pretty new.

var justBoxesCoords = {
    "type": "FeatureCollection",
    "name": "Just_Boxes",
    "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
    "features": [
    { "type": "Feature", "properties": { "OID_": 0, "Shape_Leng": 64756.395995600004, "Shape_Area": 226565856.76100001, "NAME": "BOX A" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -80.774356391317028, 27.822310348006031 ], [ -80.742708836075678, 27.822255003215457 ], [ -80.742852235235631, 27.761330877183124 ], [ -80.774482152356711, 27.76138608013256 ], [ -80.774356391317028, 27.822310348006031 ] ] ] ] } },
    { "type": "Feature", "properties": { "OID_": 0, "Shape_Leng": 53766.407119000003, "Shape_Area": 160601474.13800001, "NAME": "BOX B" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -80.724177015589376, 27.80710446026918 ], [ -80.668736433230038, 27.806982625663125 ], [ -80.668811150957822, 27.782334107834661 ], [ -80.724239228912936, 27.782455816025475 ], [ -80.724177015589376, 27.80710446026918 ] ] ] ] } },
    { "type": "Feature", "properties": { "OID_": 0, "Shape_Leng": 22656.284760800001, "Shape_Area": 32081702.447799999, "NAME": "BOX C" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -80.724253304004648, 27.776874978683274 ], [ -80.706736796747734, 27.776838925234912 ], [ -80.706778562939078, 27.761259087427607 ], [ -80.724292575595669, 27.76129511721139 ], [ -80.724253304004648, 27.776874978683274 ] ] ] ] } }
    ]
};

// original layer to display geoJson
var justBoxes = L.geoJson(justBoxesCoords,{
    onEachFeature: function(feature,layer){
        layer.bindTooltip("LOOK AT ME<br>I'M SO COOL",{permanent:true,direction:'center'});
    }
}).addTo(map);
// layer to place clipped features
var myLayer = L.geoJSON().addTo(map);

map.on('dragend',resetMidpoint);
map.on('zoomend',resetMidpoint);

function resetMidpoint(){
    // remove original geoJson layer from map
    if (map.hasLayer(justBoxes)){
        map.removeLayer(justBoxes);
    }
    // remove clipped features, if exist
    if (map.hasLayer(myLayer)){
        myLayer.clearLayers();
    }
    // get ends of frame and create bounding box
    var minX = map.getBounds().getWest();
    var minY = map.getBounds().getSouth();
    var maxX = map.getBounds().getEast();
    var maxY = map.getBounds().getNorth();
    var bbox = [minX, minY, maxX, maxY];
    // loop through original geoJson features
    justBoxes.eachLayer(function(layer){
        // get coordinates for each feature
        var array = layer.feature.geometry.coordinates[0];
        // clip feature to bounding box
        var newFeat = turf.bboxClip(turf.polygon(array),bbox);
        // if clipped feature has area > 0 (meaning it's visible in frame) then add to clipped layer
        if (turf.area(newFeat)>0){
            myLayer.addData(newFeat);
        }
    });
    // label each clipped feature
    myLayer.eachLayer(function(layer){
        layer.bindTooltip("LOOK AT ME<br>I'M SO COOL",{permanent:true,direction:'center'}).openTooltip();
    });
}
  • I'm afraid there is no native out-of-the-box solution and Turf.js is definitely the best candidate for this kind of a job. There shouldn't be more than 20 to 30 lines of code for the solution. – TomazicM Jun 19 at 20:25
  • Cool beans, thanks for the info! Owl take a stab at it. – naltizer Jun 19 at 20:40
  • 1
    One could reuse Leaflet's built-in polygon-clipping features (thus negating the need for using Turf), but that's about it. It'd be possible to gain performance by reusing the clipped (and simplified) polygon data before it's (re-)sent to a L.Renderer, at the cost of code complexity. – IvanSanchez Jun 20 at 7:54
  • I opted with the Turf.js route since I'm unfamiliar with extending Leaflet and didn't feel comfortable taking a stab at it just yet. Original post has been edited with my result. – naltizer Jul 7 at 22:56
1

It's possible to reuse Leaflet's built-in polygon-clipping features and even hook into the internals of Leaflet to actually reuse the clipped polygon data that gets passed to a L.Renderer every time a L.Path is about to be re-drawn.

It's a bit hackish, it requires knowledge on how to extend Leaflet and reading the Leaflet source code (specifically about L.Polygon and L.Renderer), but it's doable.

My approach would be for polygons to fire a reclipped event every time they're about to be re-drawn, providing the points for the clipped polygon. I'd do that by re-writing the internal _update method, which calls the internal _clipPoints method, which updates the internal _parts property, which contains the clipped points, i.e.:

L.Polygon.include({
    _update: function () {
        if (!this._map) { return; }

        this._clipPoints();
        this._simplifyPoints();
        this._updatePath();
        this.fire('reclipped', {parts: this._parts})
    }
});

Then, hook an event listener for that, massage the data a bit, apply your favourite polygon center/centroid algorithm (mine is polylabel), and update the center symbol as needed, e.g.:

    var centerSymbol;
    poly.on('reclipped', function(ev) {
        // Transform the data so that it fits polylabel's format
        var parts = ev.parts.map(function(ring){
            return ring.map(function(point){
                if ('x' in point) {
                    return [point.x, point.y]
                }
            });
        });

        if (!parts.length) {
            // Polygon not visible, do not calculate center, hide symbol
            if (centerSymbol) centerSymbol.remove();
            return;
        }

        var centerpoint = polylabel(parts, 1);
        var centerLatLng = map.layerPointToLatLng(centerpoint);
        // console.log(feat.properties.name_long, parts, centerpoint, centerLatLng);

        if (centerSymbol) {centerSymbol.setLatLng(centerLatLng).addTo(map)} 
        else { centerSymbol = L.circleMarker(centerLatLng, {color: 'red', radius:10}).addTo(map)}
        
    })

There's a fair deal of transforming data there, since the internals of Leaflet project the polygon points to screen-relative coordinates (using pixels as units), so the internal _parts contain coordinates in pixels. That's why there's a call to map.layerPointToLatLng().

If you're using GeoJSON, you can use onEachFeature as a closure to do all that to every polygon, e.g.

var stuff = L.geoJson(euCountries, {
    onEachFeature: function(feat, poly){
        var centerSymbol;
        poly.on('reclipped', function(ev) {
          /* etc */
        });
    }
});

That's missing sanity checks (what if there are LineStrings in the GeoJSON data?), it doesn't really work for multipolygons (polylabel only calculates a centerpoint inside the first ring, it seems), it's not using text labels (replace L.CircleMarker with a L.Tooltip or with a L.Marker with a L.DivIcon), but it works.

See a working example here.

| improve this answer | |
  • Thanks for reminder about L.PolyUtil.clipPolygon, it completely slipped from my mind. – TomazicM Jun 20 at 9:47

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.