Select to view content in your preferred language

View.takeScreenShot does not include scalebar

1085
2
12-03-2023 08:46 PM
YangJin
Emerging Contributor

Hi everyone, 

in my case, I added the scalebar to the mapview.ui. However, when I use MapView.takeScreenshot to export the current view, it doesn't include the scalebar.  

Does anyone know how to add scalebar to the screenshot? 

current_mapview.pngexported_screenshot.png

 

0 Kudos
2 Replies
ReneRubalcava
Esri Frequent Contributor

The takeScreenshot method only includes map elements, not widgets or other DOM elements.

https://developers.arcgis.com/javascript/latest/api-reference/esri-views-MapView.html#takeScreenshot

Some people have reported using html2canvas to capture DOM elements, but I haven't tried it with the Maps SDK.

0 Kudos
JohnEllisDCP
New Contributor

Hello everyone,

For anyone trying to draw custom elements like a scale bar or a North arrow onto a screenshot taken with view.takeScreenshot(), here is a complete solution that might help. The key is to take the screenshot first, then draw it onto a temporary HTML canvas where you can add any custom graphics you need.

The Core Logic: takeScreenshotWithOverlays

This is the main function that orchestrates the process. It takes the screenshot, creates a temporary canvas, and calls the helper functions to draw the scale bar and North arrow.

/**
 * Takes a screenshot of the current map view and draws custom overlays on it.
 * @param {esri/views/MapView} view - The MapView instance to capture.
 * @param {HTMLElement} container - The HTML element where the final image will be displayed.
 */
async function takeScreenshotWithOverlays(view, container) {
    try {
        // 1. Take the initial screenshot from the MapView.
        // This returns a promise that resolves with a Screenshot object.
        const screenshot = await view.takeScreenshot({
            format: "png",
            width: 600,
            height: 338
        });

        // 2. Create an in-memory Image object. This is necessary to handle the
        // raw image data from the screenshot before drawing it on a canvas.
        const image = new Image();

        // 3. Define the 'onload' event handler. This code will execute *after*
        // the screenshot data has been successfully loaded into the Image object.
        image.onload = function() {
            // 4. Create a temporary canvas element to act as our drawing board.
            const canvas = document.createElement('canvas');
            const context = canvas.getContext('2d');
            const canvasWidth = image.width;
            const canvasHeight = image.height;
            canvas.width = canvasWidth;
            canvas.height = canvasHeight;

            // 5. Draw the original map screenshot as the base layer on our canvas.
            context.drawImage(image, 0, 0);

            // 6. Call our custom functions to draw the overlays on top of the map image.
            // These functions need access to the canvas's drawing context and the map view.
            drawCustomScaleBar(context, view, canvasWidth, canvasHeight);
            drawNorthArrow(context, view, canvasWidth, canvasHeight);

            // 7. Create the final image element that will be displayed in the report.
            const imageElement = document.createElement("img");

            // 8. Convert the entire canvas (map + overlays) into a PNG data URL
            // and set it as the source for our final image element.
            imageElement.src=canvas.toDataURL('image/png');

            // 9. Clear the target container and append the new, final image.
            container.innerHTML = '';
            container.appendChild(imageElement);
        };

        // 10. Set the source of the Image object to the screenshot's data URL.
        // This action triggers the 'onload' event handler defined above.
        image.src=screenshot.dataUrl;

    } catch (err) {
        console.error("Screenshot failed:", err);
        container.innerHTML = '<em>Could not generate map image.</em>';
    }
}

Helper Function: drawCustomScaleBar

This function calculates an appropriate scale based on the map's current extent and draws a scale bar in the bottom-left corner.

  • Important Note: This method assumes your MapView is using a projected coordinate system (like UTM or State Plane) where the extent's units are in meters and suitable for planar measurement. It will not be accurate for views using Web Mercator (wkid: 3857), as its "meters" are distorted and do not represent a true ground distance.

/**
 * Draws a dynamic scale bar onto a canvas context.
 * @param {CanvasRenderingContext2D} context - The context of the canvas to draw on.
 * @param {esri/views/MapView} view - The MapView, used to get the current map scale.
 * @param {number} canvasWidth - The width of the canvas.
 * @param {number} canvasHeight - The height of the canvas.
 */
function drawCustomScaleBar(context, view, canvasWidth, canvasHeight) {
    // The view's extent width is in meters. We convert it to feet for our scale bar.
    const realWorldWidthInFeet = view.extent.width * 3.28084;
    const pixelWidth = canvasWidth;
    const pixelsPerFoot = pixelWidth / realWorldWidthInFeet;
    const oneMileInFeet = 5280;

    // Determine a "nice" length for the scale bar (e.g., 1 mile, 500 feet)
    // by checking what distance would occupy about 40% of the image width.
    let scaleBarDistanceInFeet = oneMileInFeet * 5; // Start with 5 miles
    if (scaleBarDistanceInFeet * pixelsPerFoot > pixelWidth * 0.4) scaleBarDistanceInFeet = oneMileInFeet * 2;
    if (scaleBarDistanceInFeet * pixelsPerFoot > pixelWidth * 0.4) scaleBarDistanceInFeet = oneMileInFeet;
    if (scaleBarDistanceInFeet * pixelsPerFoot > pixelWidth * 0.4) scaleBarDistanceInFeet = 2000; // feet
    if (scaleBarDistanceInFeet * pixelsPerFoot > pixelWidth * 0.4) scaleBarDistanceInFeet = 1000;
    if (scaleBarDistanceInFeet * pixelsPerFoot > pixelWidth * 0.4) scaleBarDistanceInFeet = 500;
    if (scaleBarDistanceInFeet * pixelsPerFoot > pixelWidth * 0.4) scaleBarDistanceInFeet = 200;
    if (scaleBarDistanceInFeet * pixelsPerFoot > pixelWidth * 0.4) scaleBarDistanceInFeet = 100;
    if (scaleBarDistanceInFeet * pixelsPerFoot > pixelWidth * 0.4) scaleBarDistanceInFeet = 50;
    if (scaleBarDistanceInFeet * pixelsPerFoot > pixelWidth * 0.4) scaleBarDistanceInFeet = 20;
    if (scaleBarDistanceInFeet * pixelsPerFoot > pixelWidth * 0.4) scaleBarDistanceInFeet = 10;

    const scaleBarWidthInPixels = Math.round(scaleBarDistanceInFeet * pixelsPerFoot);

    // Create a clean label (e.g., "1 mile" or "500 feet").
    let label = '';
    if (scaleBarDistanceInFeet >= oneMileInFeet) {
        const miles = scaleBarDistanceInFeet / oneMileInFeet;
        label = `${miles} ${miles === 1 ? 'mile' : 'miles'}`;
    } else {
        label = `${Math.round(scaleBarDistanceInFeet)} feet`;
    }

    // Define position and appearance.
    const barX = 20;
    const barY = canvasHeight - 40;
    const barHeight = 14;

    // Draw the scale bar rectangle.
    context.fillStyle = 'rgba(0, 0, 0, 0.8)';
    context.strokeStyle = 'rgba(255, 255, 255, 1)';
    context.lineWidth = 1.5;
    context.fillRect(barX, barY, scaleBarWidthInPixels, barHeight);
    context.strokeRect(barX, barY, scaleBarWidthInPixels, barHeight);

    // Draw the text label inside the bar.
    context.fillStyle = 'rgba(255, 255, 255, 0.9)';
    context.font = 'bold 14px Inter, sans-serif';
    context.textAlign = 'center';
    context.textBaseline = 'middle';
    context.fillText(label, barX + scaleBarWidthInPixels / 2, barY + barHeight / 2);
}

Helper Function: drawNorthArrow

This function draws a North arrow that rotates based on the map's rotation and includes the angle as text.

/**
 * Draws a dynamic North arrow onto a canvas context.
 * @param {CanvasRenderingContext2D} context - The context of the canvas to draw on.
 * @param {esri/views/MapView} view - The MapView, used to get the current map rotation.
 * @param {number} canvasWidth - The width of the canvas.
 * @param {number} canvasHeight - The height of the canvas.
 */
function drawNorthArrow(context, view, canvasWidth, canvasHeight) {
    const rotation = view.rotation; // Get the rotation in degrees from the view.
    const size = 25; // Size of the arrow body.
    const margin = 30; // Margin from the edges.
    const centerX = canvasWidth - margin - size;
    const centerY = canvasHeight - margin - size; // Position from bottom-right.

    context.save(); // Save the canvas state before we transform it.

    // Move the canvas origin to the arrow's location and rotate the entire canvas.
    context.translate(centerX, centerY);
    context.rotate(rotation * Math.PI / 180); // Convert degrees to radians for canvas API.

    // Draw the arrow shape.
    context.beginPath();
    context.moveTo(0, -size);
    context.lineTo(size / 2, size);
    context.lineTo(0, size / 2);
    context.lineTo(-size / 2, size);
    context.closePath();

    context.fillStyle = 'rgba(0, 0, 0, 0.8)';
    context.fill();
    context.strokeStyle = 'rgba(255, 255, 255, 1)';
    context.lineWidth = 1.5;
    context.stroke();

    // Draw the 'N' inside the arrow.
    context.fillStyle = 'white';
    context.font = 'bold 16px Inter, sans-serif';
    context.textAlign = 'center';
    context.textBaseline = 'middle';
    context.fillText('N', 0, 0);

    context.restore(); // Restore the canvas to its original, unrotated state.

    // --- Draw the rotation degrees text with a "halo" for visibility ---
    const angleText = `${rotation.toFixed(1)}°`;
    const textX = centerX;
    const textY = centerY - size - 10; // Position text above the arrow.

    context.font = 'bold 12px Inter, sans-serif';
    context.textAlign = 'center';

    // 1. Draw the white "halo" by stroking the text.
    context.strokeStyle = 'white';
    context.lineWidth = 3;
    context.lineJoin = 'round';
    context.strokeText(angleText, textX, textY);

    // 2. Draw the black text on top of the halo.
    context.fillStyle = 'black';
    context.fillText(angleText, textX, textY);
}

Hope this helps!

Also I feel I should note that Gemini AI was used to add comments and clarify some of my poor directions.

0 Kudos