Search code examples
svgmobileleafletzooming

SVG Zoom Issue Desktop vs Mobile with leaflet.js


I've tried to use leaflet.js to allow user to zoom and pan on big SVG files on my website. I've used the following script to display the SVG with leaflet :

    // Using leaflet.js to pan and zoom a big image.
    // See also: http://kempe.net/blog/2014/06/14/leaflet-pan-zoom-image.html
    var factor = 1;
    
    // create the slippy map
    var map = L.map('image-map', {
        minZoom: 1,
        maxZoom: 5,
        center: [0, 0],
        zoom: 1,
        crs: L.CRS.Simple
    });

    function getMeta(url) {
        const img = new Image();
        img.addEventListener("load", function () {
            var w = this.naturalWidth;
            var h = this.naturalHeight;
            var southWest = map.unproject([0, h], map.getMaxZoom() - 1);
            var northEast = map.unproject([w, 0], map.getMaxZoom() - 1);
            var bounds = new L.LatLngBounds(southWest, northEast);


            // add the image overlay, 
            // so that it covers the entire map
            L.imageOverlay(img.src, bounds).addTo(map);

            // tell leaflet that the map is exactly as big as the image
            map.setMaxBounds(bounds);
            map.fitBounds(bounds); // test

        });
        img.src = url;
    }

    getMeta("/assets/images/bhikkhu-patimokkha.svg")

You can see the result here. The probleme is that it works fine on my iPhone, the zoom level are approriate and it is easy to zoom in, but on the desktop version, you can only zoom out and not in.

I've tried to change the minZoom: 1 to different values, but it dosen't seem to do anything. The only way I was able to make it work on desktop was to add a multiplicating factor 10 to var w and var h, but then it would screw up on the mobile version.

I had more sucess with PNG images, but they are very slow to load. Maybe the code is inapropriate for SVG and particularly when calling naturalHeight ? But it looked fined when I debbuged it.

Thanks for your help.

Here is a codepen to play around if you want.

EDIT : Using the nicer code from @Grzegorz T. It works well on Desktop now. but on Safari iOS it is overzoomed and cannot unzoom... see picture below (it was working with the previous code on Iphone but not on Destop...)

iphone capture


Solution

  • Display the variables w and h you will see what small variables are returned. To increase them I increased them by * 5 for this I used fitBounds and it should now scale to the viewer window and at the same time it is possible to zoom.

    To be able to also click more than once on the zoom, I changed map.getMaxZoom () - 1 to map.getMaxZoom () - 2

    var map = L.map("image-map", {
      minZoom: 1, // tried 0.1,-4,...
      maxZoom: 4,
      center: [0, 0],
      zoom: 2,
      crs: L.CRS.Simple
    });
    
    function getMeta(url) {
      const img = new Image();
      img.addEventListener("load", function () {
        var w = this.naturalWidth * 5;
        var h = this.naturalHeight * 5;
    
        var southWest = map.unproject([0, h], map.getMaxZoom() - 2);
        var northEast = map.unproject([w, 0], map.getMaxZoom() - 2);
        var bounds = new L.LatLngBounds(southWest, northEast);
    
        // add the image overlay,
        // so that it covers the entire map
        L.imageOverlay(img.src, bounds).addTo(map);
    
        // tell leaflet that the map is exactly as big as the image
        // map.setMaxBounds(bounds);
        map.fitBounds(bounds);
      });
      img.src = url;
    }
    
    getMeta("https://fractalcitta.github.io/assets/images/bhikkhu-patimokkha.svg");
    
    

    You don't need to increase (w, h) * 5 but just change to map.getMaxZoom () - 4

    And one more important thing that you should always do with svg, which is optimizing these files. I always use this site - svgomg

    Second version width async/promise ----------

    let map = L.map("map", {
      crs: L.CRS.Simple,
      minZoom: 1,
      maxZoom: 4,
    });
    
    function loadImage(url) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.addEventListener("load", () => resolve(img));
        img.addEventListener("error", reject);
        img.src = url;
      });
    }
    
    async function getImageData(url) {
      const img = await loadImage(url);
      return { img, width: img.naturalWidth, height: img.naturalHeight };
    }
    
    async function showOnMap(url) {
      const { img, width, height } = await getImageData(url);
    
      const southWest = map.unproject([0, height], map.getMaxZoom() - 4);
      const northEast = map.unproject([width, 0], map.getMaxZoom() - 4);
    
      const bounds = new L.LatLngBounds(southWest, northEast);
      L.imageOverlay(img.src, bounds).addTo(map);
    
      map.fitBounds(bounds);
    }
    
    showOnMap(
      "https://fractalcitta.github.io/assets/images/bhikkhu-patimokkha.svg"
    );
    

    The third approach to the problem, I hope the last one ;)

    You need a little description of what's going on here. We get svg by featch we inject into hidden div which has width/height set to 0
    Then we use the getBBox() property to get the exact dimensions from that injected svg.

    I am not using map.unproject in this example. To have the exact dimensions of bounds it is enough that:

    const bounds = [
      [0, 0], // padding
      [width, height], // image dimensions
    ];
    

    All code below:

    <div id="map"></div>
    <div id="svg" style="position: absolute; bottom: 0; left: 0; width: 0; height: 0;"></div>
    
    let map = L.map("map", {
      crs: L.CRS.Simple,
      minZoom: -4,
      maxZoom: 1,
    });
    
    const url =
      "https://fractalcitta.github.io/assets/images/bhikkhu-patimokkha.svg";
    
    async function fetchData(url) {
      try {
        const response = await fetch(url);
        const data = await response.text();
        return data;
      } catch (err) {
        console.error(err);
      }
    }
    
    fetchData(url)
      .then((svg) => {
        const map = document.getElementById("svg");
        map.insertAdjacentHTML("afterbegin", svg);
      })
      .then(() => {
        const svgElement = document.getElementById("svg");
        const { width, height } = svgElement.firstChild.getBBox();
        return { width, height };
      })
      .then(({ width, height }) => {
        const img = new Image();
        img.src = url;
    
        const bounds = [
          [0, 0], // padding
          [width, height], // image dimensions
        ];
    
        L.imageOverlay(img.src, bounds).addTo(map);
    
        map.fitBounds(bounds);
      });