Leaflet.js SimpleMapScreenshoter – Troubleshooting Invisible Polygon Fill Patterns

leafletleaflet-plugins

We are using Leaflet to display polygons with fill patterns using the leaflet-polygon.fillPattern.js plugin. Additionally, we are using the leaflet-simple-map-screenshoter.js plugin to allow users to take screenshots of the map.

The problem is that the fill pattern image is not visible in the screenshot taken with the simpleMapScreenshoter plugin. We have tried using both JPEG and PNG images as fill patterns, but the result is the same.

We have made sure that the fill pattern image URL is accessible from the same domain as the map and that the image is loaded before taking the screenshot. We have also tried setting the crossOrigin property of the image to anonymous as suggested in some other posts, but it did not help.

Are there any additional steps we need to take to make the fill pattern visible in the screenshot taken with the simpleMapScreenshoter plugin?

The related codepen is here: https://codepen.io/pavanyogi/pen/MWqEgea

Here are two images, with the screenshot on the right.
Here are two images, with the screenshot on the right

Here are a few related so threads.
Hatch polygons in Leaflet

Here is the related code:

<!DOCTYPE html>

<head>

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css" crossorigin="anonymous" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.js"></script>

    <script src="https://lwsu.github.io/leaflet-polygon-fillPattern/leaflet-polygon.fillPattern.js"></script>

    <script src="https://unpkg.com/[email protected]/dist/leaflet-simple-map-screenshoter.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/FileSaver.js"></script>

    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
            overflow: hidden;
        }

        #map {
            width: 400px;
            height: 400px;
        }
    </style>
</head>

<body>
    <div id="map"></div>
    <!-- <div id="mapSimple"></div> -->
    <div id="mapSimpleScreeningState"></div>
    <script>
        var map = L.map('map').setView([23.7, 121], 10);
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {}).addTo(map);

        var poly1 = [
            [24, 121],
            [24.5, 121],
            [24.5, 121.9],
            [24, 121.9]
        ];
        L.polygon(poly1, {
            fill: 'url(cross-hatch-black.png)',
            fillOpacity: 1
        }).addTo(map);


        var poly2 = [
            [24, 120],
            [23.5, 120],
            [23.5, 120.9],
            [24, 120.9]
        ];
        L.polygon(poly2, {
            fill: 'url(cross-hatch-black.png)',
            fillOpacity: 1
        }).addTo(map);


        let pluginOptions = {
            cropImageByInnerWH: true, // crop blank opacity from image borders
            hidden: false, // hide screen icon
            preventDownload: false, // prevent download on button click
            domtoimageOptions: {}, // see options for dom-to-image
            position: 'topleft', // position of take screen icon
            screenName: 'screen', // string or function
            // iconUrl: ICON_SVG_BASE64, // screen btn icon base64 or url
            // hideElementsWithSelectors: ['.leaflet-control-container'], // by default hide map controls All els must be child of _map._container
            mimeType: 'image/png', // used if format == image,
            caption: null, // string or function, added caption to bottom of screen
            captionFontSize: 15,
            captionFont: 'Arial',
            captionColor: 'black',
            captionBgColor: 'white',
            captionOffset: 5,
            // callback for manually edit map if have warn: "May be map size very big on that zoom level, we have error"
            // and screenshot not created
            onPixelDataFail: async function({
                node,
                plugin,
                error,
                mapPane,
                domtoimageOptions
            }) {
                // Solutions:
                // decrease size of map
                // or decrease zoom level
                // or remove elements with big distanses
                // and after that return image in Promise - plugin._getPixelDataOfNormalMap
                return plugin._getPixelDataOfNormalMap(domtoimageOptions)
            }
        }

        // init plugin
        L.simpleMapScreenshoter(pluginOptions).addTo(map);

        // optional: add screening state info
        map.on('simpleMapScreenshoter.takeScreen', function() {
            document.getElementById('mapSimpleScreeningState').innerHTML += 'screening...<br>'
        })
        map.on('simpleMapScreenshoter.done', function() {
            document.getElementById('mapSimpleScreeningState').innerHTML += 'screen end...<br>'
        })
        map.on('simpleMapScreenshoter.error', function(event) {
            console.error(event.e)
            document.getElementById('mapSimpleScreeningState').innerHTML += event.e.toString() + '<br>'
        })
    </script>
</body>

</html>

Best Answer

Screen shooting part of leaflet-simple-map-screenshoter plugin is based on dom-to-image-more library (see https://github.com/1904labs/dom-to-image-more). If you look at the plugin docs, you can find this statement there:

If the DOM node you want to render includes a element with something drawn on it, it should be handled fine, unless the canvas is tainted - in this case rendering will rather not succeed.

So obviously problem here is tainted canvas, which is consequence of using ouside source for polygon fill.

Cure for this can be creating fill pattern in the code. It can be either canvas or SVG. To use either of these, data URL has to be created, but this then poses problem for leaflet-simple-map-screenshoter plugin, since it uses URL string as part of id for polygon fill pattern. To make it work, one line of plugin has to be modified:

//  var _ref_id = _img_url + (Math.random() * Math.pow(10, 17) + Math.random() * Math.pow(10, 17));
var _ref_id = (Math.random() * Math.pow(10, 17) + Math.random() * Math.pow(10, 17));

Here is an example of creating polygon fill pattern with canvas and SVG:

var size = 20;
var lineWidht = 4;
var offset = lineWidht + 1;
var color = '#FF0000';

var myCanvas = document.createElement('canvas');
myCanvas.width  = size;
myCanvas.height = size;

var drawingContext = myCanvas.getContext("2d");  
drawingContext.beginPath();
drawingContext.strokeStyle = color;
drawingContext.lineWidth = lineWidht;  
drawingContext.moveTo(-offset, 0);
drawingContext.lineTo(size, (size + offset));
drawingContext.moveTo((2 * offset), -offset);
drawingContext.lineTo((size + offset), (2 * offset));
drawingContext.stroke();

var dataURL1 = myCanvas.toDataURL();
var dataURL2 = "data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' id='svg8'%3E%3Cpattern id='pattern-checkers' x='0' y='0' width='20' height='20'%3E%3Crect x='0' width='10' height='10' y='0' fill='gray'%3E%3C/rect%3E%3Crect x='10' width='10' height='10' y='10' fill='gray'%3E%3C/rect%3E%3C/pattern%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='url(%23pattern-checkers)'%3E%3C/rect%3E%3C/svg%3E";

var map = L.map('map').setView([23.7, 121], 9);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
}).addTo(map);

var poly1 = [
    [24, 121],
    [24.5, 121],
    [24.5, 121.9],
    [24, 121.9]
];
L.polygon(poly1, {fill: 'url(' + dataURL1 + ')', fillOpacity: 0.7}).addTo(map);


var poly2 = [
    [24, 120],
    [23.5, 120],
    [23.5, 120.9],
    [24, 120.9]
];
L.polygon(poly2, {fill: 'url(' + dataURL2 + ')', fillOpacity: 0.7}).addTo(map);


let pluginOptions = {
   cropImageByInnerWH: true, // crop blank opacity from image borders
   hidden: false, // hide screen icon
   preventDownload: false, // prevent download on button click
   domtoimageOptions: {}, // see options for dom-to-image
   position: 'topleft', // position of take screen icon
   screenName: 'screen', // string or function
   mimeType: 'image/png', // used if format == image,
   caption: null, // string or function, added caption to bottom of screen
   captionFontSize: 15,
   captionFont: 'Arial',
   captionColor: 'black',
   captionBgColor: 'white',
   captionOffset: 5,
   onPixelDataFail: async function({ node, plugin, error, mapPane, domtoimageOptions }) {
       return plugin._getPixelDataOfNormalMap(domtoimageOptions)
   }
}

L.simpleMapScreenshoter(pluginOptions).addTo(map);

This is map screen shot:

enter image description here

And this is image produced with plugin:

enter image description here