dlyzo
dlyzo

Reputation: 128

Take a screenshot of leaflet map

So i have a leaflet map, on which i drew some layers(rectangles). I want to capture a screenshot of that particular layer, not the whole visible map. I've tried leaflet plugin, but they didn't work as i was expecting them to, and i've managed to capture the screenshot, but of the whole visible part of the map using html2canvas. How could i select (capture) only that rectangle, which i want to be in the screenshot?

Or maybe its possible to capture selected area while using leaflet-area-select?

Im using react and typescript.

Upvotes: 1

Views: 10034

Answers (2)

Danrley Pereira
Danrley Pereira

Reputation: 1366

Use a Ref to Access the Map: Utilize useState and useEffect to work with the map reference, ensuring that the map is fully loaded and your desired elements are rendered before taking the screenshot.

Capture and Handle the Screenshot: Implement the logic to capture the screenshot and handle the resulting image, such as logging or saving it.

I am using react-leaflet library so I came with something like this:

import L from "leaflet";
import { MapContainer } from "react-leaflet";
// Import SimpleMapScreenshoter after leaflet
import { SimpleMapScreenshoter } from 'leaflet-simple-map-screenshoter';
... // Inside your Map container component
const [mapRef, setMapRef] = useState();

useEffect(() => {
  if (mapRef) {
    const snapshotOptions = {
      hideElementsWithSelectors: ["leaflet-control-simpleMapScreenshoter"], // by default hide map controls All els must be child of _map._container
      position: "topright",
      screenName: 'MapWithWellbore',
      hidden: false,
    };
    const simpleMapScreenshoter = new SimpleMapScreenshoter(snapshotOptions).addTo(mapRef);
    
    // Capture and handle screenshot
    simpleMapScreenshoter.takeScreen("image")
      .then(base64Image => {
        console.log(base64Image); // Process the image as needed
      })
      .catch(e => {
        console.error(e.toString());
      });
  }
}, [mapRef]);

return (
  <MapContainer ref={setMapRef} attributionControl={false}>
    // Your map components here
  </MapContainer>
);

// Note: Ensure all map elements are rendered before taking the screenshot

Remember to handle cases where certain map elements might not be fully rendered when the screenshot is taken. Good luck with your endevour!

Take a thorough look at the library: https://github.com/grinat/leaflet-simple-map-screenshoter

Upvotes: 0

Seth Lutske
Seth Lutske

Reputation: 10686

This can be done with the help of leaflet-simple-map-screenshoter, and some careful image manipulation.

Working codesandbox

Here's a walkthrough of what I did:

Custom panes

First, lets create some custom panes to separate out what we want in the shot from what we dont. Really all you're going to need is what layers you dont want in the screenshot, but we'll create two for good measure:

//Create a map and assign it to the map div
var map = L.map("leafletMapid", mapOptions);

// Create some custom panes
map.createPane("snapshot-pane");
map.createPane("dont-include");

Now we can create our layers within those panes, whether they're tile layers or geojson or paths or whatever. Lets include the baselayer and a geojson we want in the shot in the "snapshot-pane", and a path we dont want in the shot in the "dont-include" pane:

// Add baselayer and geojson to snapshot pane
const baselayer = L.tileLayer(url,
  { pane: "snapshot-pane" }
).addTo(map);
const greekborder = L.geoJSON(greekBorderGeoJson, {
  pane: "snapshot-pane"
}).addTo(map);

// Add another polygon to the 'dont-include' pane
const serbianborder = L.polyline(serbianBorder, {
  color: "darkred",
  pane: "dont-include"
}).addTo(map);

Setting up a screenshotter:

Using the screenshot plugin, let's set up a screenshotter. The screenshotter will exclude the layers we set in the "dont-include" pane:

const snapshotOptions = {
  hideElementsWithSelectors: [
    ".leaflet-control-container",
    ".leaflet-dont-include-pane",
    "#snapshot-button"
  ],
  hidden: true
};

// Add screenshotter to map
const screenshotter = new SimpleMapScreenshoter(snapshotOptions);
screenshotter.addTo(map);

Custom screenshot function

We don't want to use the default screenshot behavior, so we'll assign a function to fire when we hit a custom button

// define screenshot function
const takeScreenShot = () => {
  // defined below
}

// Add takescreenshot function to button
const button = document.getElementById("snapshot-button");
button.addEventListener("click", takeScreenShot);

If you check out the docs from the screenshot plugin, you can catch the image data created by the screenshotter in the .then that comes after the screenshot is taken:

const takeScreenShot = () => {
  
screenshotter
    .takeScreen("image")
    .then((image) => {
      // Create <img> element to render img data
      var img = new Image();
    })

}

We've grabbed the image data, create a new Image element, and we're ready to assign the image data to the new Image element. But before we do, we want to define an img.onload function that will fire at the moment the image data is assigned to the Image element. This onload must be defined before we can assign the image source data.

What happens when the image loads

When the image loads, we want to do a few things

  1. Find out the bounds of the relevant map feature
  2. Translate those bounds to pixel coordinates on the screen
  3. Write the image data contained only within those bounds to a canvas
  4. Save that canvas as a png file

Here is the code to do that:

img.onload = () => {
  // Create canvas to process image data
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");

  // Set canvas size to the size of your resultant image
  canvas.width = imageSize.x;
  canvas.height = imageSize.y;

  // Draw just the portion of the whole map image that contains
  // your feature to the canvas
  ctx.drawImage(
    img,
    topLeft.x,
    topLeft.y,
    imageSize.x,
    imageSize.y,
    0,
    0,
    imageSize.x,
    imageSize.y
  );

  // Create URL for resultant png
  var imageurl = canvas.toDataURL("image/png");

  const resultantImage = new Image();
  resultantImage.style = "border: 1px solid black";
  resultantImage.src = imageurl;

  // Append new image to body for nice visual in this example answer
  document.body.appendChild(canvas);

  canvas.toBlob(function (blob) {
    // saveAs function installed as part of leaflet snapshot package
    saveAs(blob, "greek_border.png");
  });
};

Take your time to go through that and let me know if you have any questions.

An Image.onload function fires when the image src is assigned, so we define the function first, then assign the src:

const takeScreenShot = () => {
  
screenshotter
    .takeScreen("image")
    .then((image) => {
      // Create <img> element to render img data
      var img = new Image();

      img.onload = () => {
        // all that code in the prev code block
      }

      // set the image source to what the snapshotter captured
      // img.onload will fire AFTER this
      img.src = image;
    })

}

And that's it. You'll get an image downloaded that contains all layers of the map, except the controls, and whatever you excluded in the "dont-include" pane. The image will be cropped to the bounds of your feature.

Note this will not work if part of the feature is outside of the visible map bounds. While it is possible to achieve what you want with part of the feature outside of the map bounds, it is far more complicated to do that, especially if you want the baselayer in the background.

Also, you said initially you were using react. I highly recommend using react-leaflet if you're using leaflet in a react project. I initially wrote this answer as a react-leaflet answer, before you specified that you're not using it, but here is the react-leaflet version:

React-leaflet codesandbox

Upvotes: 5

Related Questions