[GIS] Resize vector marker according to zoom (i.e. marker constant size in meters) in Leaflet

leafletmarkerszoom

I have a map with 20 markers on a geojson layer. They are all from the same SVG and represent a certain area on the map (meaning they represent a circle of constant radius in meter on the map). I need these marker to adapt their size according to zoom level.

I've tried to use a circle as a marker. But my marker needs to be a SVG because it is complex graphic and radius doesn't apply to markers.

I think each time zoom level changes marker size should be resized proportionally to the new zoom level but I don't know how to achieve this.

He's how the markers are displayed, parsed from a csv sheet :

    for (i in chapters) {
        var c = chapters[i];
        if (!isNaN(parseFloat(c['Latitude'])) && !isNaN(parseFloat(c['Longitude']))) {
            var lat = parseFloat(c['Latitude']);
            var lon = parseFloat(c['Longitude']);
            var cercleDirection = parseFloat(c['Direction']);
            var photoIcon = L.icon({
                                iconUrl: 'media/Cercle.svg',
                                iconSize: [220, 220],
                                iconAnchor: [110, 110],
                            });
            markers.push(
                L.marker([lat, lon], {
                    icon: photoIcon,
                    rotationAngle: cercleDirection

                }));
        } else {
            markers.push(null);
        }

Another way maybe

I did find this approach online that seems interesting. But I couldn't have more explanation from the author that remains silent :

var layer = ''; // what would be the layer name in my case ?
map.on('zoomend', function() {
    var currentZoom = map.getZoom();

    //Update X and Y based on zoom level
    var x= 50/currentZoom; //Update x 
    var y= 50/currentZoom; //Update Y         
    var LeafIcon = L.Icon.extend({
        options: {
            iconSize:     [x, y] // Change icon size according to zoom level
        }
    });
    layer.setIcon(LeafIcon);
});

Best Answer

One possible solution is to use L.svgOverlay layer for resizable markers. In this case Leaflet takes care of resizing and placement of markers when zooming.

With this approach two problems have to be resolved:

  1. L.svgOverlay needs lat,lng bounds for SVG image. For this purpose L.circle layer can be used. Once created with given radius in meters, bounds can be obtained with getBounds() method. Since getBounds() method only works after layer is added to the map, it is added to temporary group layer which is removed at the end.
  2. Dynamically rotating SVG image used for L.svgOverlay layer is bit more complicated here. Since Leaflet adds it's own transform attribute to SVG when putting it on the map, this overrides any previous transforms, including rotation. Solution is to include SVG image as hidden inline HTML element and wrap actual graphics in <g> envelope on which rotation transform can be done dynamically.

Code below uses such hidden inline SVG element (arrow in circle), assuming that cercleDirection is angle of rotation:

<div style="display: none;">
  <svg id="svgMarker" viewBox="0 0 490.4 490.4" style="fill: blue">
    <g id="rotateElement">
      <path d="M245.2,490.4c135.2,0,245.2-110,245.2-245.2S380.4,0,245.2,0S0,110,0,245.2S110,490.4,245.2,490.4z M245.2,24.5
        c121.7,0,220.7,99,220.7,220.7s-99,220.7-220.7,220.7s-220.7-99-220.7-220.7S123.5,24.5,245.2,24.5z"/>
      <path transform="rotate(-90, 245.2, 245.2)" d="M138.7,257.5h183.4l-48,48c-4.8,4.8-4.8,12.5,0,17.3c2.4,2.4,5.5,3.6,8.7,3.6s6.3-1.2,8.7-3.6l68.9-68.9
        c4.8-4.8,4.8-12.5,0-17.3l-68.9-68.9c-4.8-4.8-12.5-4.8-17.3,0s-4.8,12.5,0,17.3l48,48H138.7c-6.8,0-12.3,5.5-12.3,12.3
        C126.4,252.1,131.9,257.5,138.7,257.5z"/>
    </g>
  </svg>
</div>

...

var tmpLayer = L.layerGroup();

var markerViewBoxWidth = 490.4;
var markerViewBoxHeight = 490.4;

var markerRadius = 5000;   // in map meters

var svgMarkerElement = document.getElementById("svgMarker");
var firstTime = true;

for (i in chapters) {
  var c = chapters[i];
  if (!isNaN(parseFloat(c['Latitude'])) && !isNaN(parseFloat(c['Longitude']))) {
    var lat = parseFloat(c['Latitude']);
    var lon = parseFloat(c['Longitude']);
    var cercleDirection = parseFloat(c['Direction']);

    var newSVG = svgMarkerElement.cloneNode(true);
    var rotateElement = newSVG.querySelector('#rotateElement');
    rotateElement.setAttribute('transform', 'rotate('+ cercleDirection + ',' + (markerViewBoxWidth / 2) + ',' + (markerViewBoxHeight / 2) + ')')

    var boundsCircle = L.circle([lat, lon], {radius: markerRadius, stroke: false, fill: false});
    boundCircle.addTo(tmpLayer);
    if (firstTime) {
      tmpLayer.addTo(map);
      firstTime = false;
    }
    var circleBounds = boundsCircle.getBounds();
    markers.push(L.svgOverlay(newSVG, circleBounds));
    } 
  else {
    markers.push(null);
  }
}

tmpLayer.remove();