Search code examples
javascriptleafletleaflet.draw

How can I grab a selection of markers with Leaflet.draw?


Context:

I've made a map, and populated it with around 300 random markers. I can 'select' the markers by clicking on a link in the popup and activate a selection to display data from. I also have the Leaflet.draw plugin to draw shapes like circles, rectangles and custom shapes, and I would like to use it to 'select' a couple of markers.

The issue

How can I grab the leaflet marker object of the markers that fall inside a drawn leaflet.draw shape so I can edit them? I cannot seem to make a selection, It either selects none of the markers, or all of them.

Code snippet, stripped from unnecessary code:

const drawControl = new L.Control.Draw({
    draw: {
        marker   : false,
        polygon  : true,
        polyline : false,
        rectangle: true,
        circle   : {
            metric: 'metric'
        }
    },
    edit: false
});

const map = L.map('map', {
    layers: [streets, light]
}).setView([CONFIG.MAP.LATITUDE, CONFIG.MAP.LONGITUDE], CONFIG.MAP.ZOOMLEVEL)

map.addControl(drawControl);

map.on(L.Draw.Event.DRAWSTOP, e => {

    const hello = e.target;

    console.log(hello);
    e.target.eachLayer(layer => {
        if (layer.options.icon) {
            console.log(layer);
        }
    });

});

Solution

  • Most of what you want can quite easily be done using Leaflet's utility methods. If you want to do this with a complex shape like L.Polygon you're going to need something like TurfJS

    For L.Circle you need to calculate the distance between the circle's center and compare it to the radius:

    var marker = new L.Marker(...),
        circle = new L.Circle(...);
    
    var contains = circle.getLatLng().distanceTo(marker.getLatLng()) < circle.getRadius();
    

    For L.Rectangle you need to fetch it's bounds object and use the contains method:

    var marker = new L.Marker(...),
        rectangle = new L.Rectangle(...);
    
    var contains = rectangle.getBounds().contains(marker.getLatLng());
    

    As said for complex polygons i'de use Turf but there are more libraries and plugins out there. Here's an example using Turf's inside method. It take a GeoJSON point and polygon feature as parameters so mind the conversion:

    var marker = new L.Marker(...),
        polygon = new L.Polygon(...);
    
    var contains = turf.inside(marker.toGeoJSON(), polygon.toGeoJSON());
    

    You could wrap those into convenience methods for each respective class:

    L.Polygon.include({
        contains: function (latLng) {
            return turf.inside(new L.Marker(latLng).toGeoJSON(), this.toGeoJSON());
        } 
    });
    
    L.Rectangle.include({
        contains: function (latLng) {
            return this.getBounds().contains(latLng);
        }
    });
    
    L.Circle.include({
        contains: function (latLng) {
            return this.getLatLng().distanceTo(latLng) < this.getRadius();
        }
    });
    
    var marker = new L.Marker(...),
        polygon = new L.Polygon(...),
        rectangle = new L.Rectangle(...),
        circle = new L.Circle(...);
    
    polygon.contains(marker.getLatLng());
    rectangle.contains(marker.getLatLng());
    circle.contains(marker.getLatLng());
    

    Note that if you implement the polygon method that there is no need for the rectangle method. Since rectangle is extended from polygon it will inherit the method. I left it in there to be complete.

    Now iterating your markers and comparing them is easy:

    map.on(L.Draw.Event.CREATED, function (e) {
        markers.eachLayer(function (marker) {
            if (!e.layer.contains(marker.getLatLng())) {
                marker.remove();
            }
        });
    });
    

    Hope that helps, here's a working snippet:

    var map = new L.Map('leaflet', {
        'center': [0, 0],
        'zoom': 0
    });
    
    var markers = new L.LayerGroup().addTo(map);
    
    for (var i = 0; i < 300; i++) {
        var marker = new L.Marker([
            (Math.random() * (90 - -90) + -90).toFixed(5) * 1,
            (Math.random() * (180 - -180) + -180).toFixed(5) * 1
        ]).addTo(markers);
    }
    
    new L.Control.Draw({
        draw: {
            marker   : false,
            polygon  : true,
            polyline : false,
            rectangle: true,
            circle   : {
                metric: 'metric'
            }
        },
        edit: false
    }).addTo(map);
    
    L.Polygon.include({
        contains: function (latLng) {
            return turf.inside(new L.Marker(latLng).toGeoJSON(), this.toGeoJSON());
        } 
    });
    
    L.Rectangle.include({
        contains: function (latLng) {
            return this.getBounds().contains(latLng);
        }
    });
    
    L.Circle.include({
        contains: function (latLng) {
            return this.getLatLng().distanceTo(latLng) < this.getRadius();
        }
    });
    
    map.on(L.Draw.Event.CREATED, function (e) {
        markers.eachLayer(function (marker) {
            if (!e.layer.contains(marker.getLatLng())) {
                marker.remove();
            }
        });
    });
    body {
        margin: 0;
    }
    
    html, body, #leaflet {
        height: 100%;
    }
    <!DOCTYPE html>
    <html>
      <head>
        <title>Leaflet 1.0.3</title>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link type="text/css" rel="stylesheet" href="//unpkg.com/[email protected]/dist/leaflet.css" />
        <link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/leaflet.draw/0.4.9/leaflet.draw.css" />
      </head>
      <body>
        <div id="leaflet"></div>
        <script type="application/javascript" src="//unpkg.com/[email protected]/dist/leaflet.js"></script>
        <script type="application/javascript" src="//cdnjs.cloudflare.com/ajax/libs/leaflet.draw/0.4.9/leaflet.draw.js"></script>
        <script type="application/javascript" src="//unpkg.com/@turf/turf@latest/turf.min.js"></script>
      </body>
    </html>