2

I am working on Mapbox. Is there any possible way to crop map image using Mapbox? Right now I am getting complete map as canvas but I just need to crop a specific area.

The thing which I want is to crop only that area where I am drawing polygon, I don't know whether it is possible or not.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Draw a polygon and calculate its area</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v2.7.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v2.7.0/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<style>
    .calculation-box {
        height: 75px;
        width: 150px;
        position: absolute;
        bottom: 40px;
        left: 10px;
        background-color: rgba(255, 255, 255, 0.9);
        padding: 15px;
        text-align: center;
    }

    p {
        font-family: 'Open Sans';
        margin: 0;
        font-size: 13px;
    }
</style>

<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.2/mapbox-gl-draw.js"></script>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.2/mapbox-gl-draw.css" type="text/css">
<div id="map"></div>
<div class="calculation-box">
    <p>Click the map to draw a polygon.</p>
    <div id="calculated-area"></div>
</div>

<script>
    mapboxgl.accessToken = 'pk.eyJ1Ijoib21lcjA5IiwiYSI6ImNreWJkcjdmczBlYmMydm9mazYwMnBxMm8ifQ.yC2TC8C5hsIFb13-HgP_Pw';
    const map = new mapboxgl.Map({
        container: 'map', // container ID
        style: 'mapbox://styles/mapbox/satellite-v9', // style URL
        center: [-91.874, 42.76], // starting position [lng, lat]
        zoom: 12 // starting zoom
    });

    const draw = new MapboxDraw({
        displayControlsDefault: false,
        // Select which mapbox-gl-draw control buttons to add to the map.
        controls: {
            polygon: true,
            trash: true
        },
        // Set mapbox-gl-draw to draw by default.
        // The user does not have to click the polygon control button first.
        defaultMode: 'draw_polygon'
    });
    map.addControl(draw);

    map.on('draw.create', updateArea);
    map.on('draw.delete', updateArea);
    map.on('draw.update', updateArea);

    function updateArea(e) {
        const data = draw.getAll();
        const answer = document.getElementById('calculated-area');
        if (data.features.length > 0) {
            const area = turf.area(data);
            // Restrict the area to 2 decimal points.
            const rounded_area = Math.round(area * 100) / 100;
            answer.innerHTML = `<p><strong>${rounded_area}</strong></p><p>square meters</p>`;
        } else {
            answer.innerHTML = '';
            if (e.type !== 'draw.delete')
                alert('Click the map to draw a polygon.');
        }
    }
</script>

</body>
</html>

MapBox-GL-JS map

2
  • You write "Right now I am getting complete map as canvas". Getting where? In what format would you like cropped map? Canvas? Please edit your question and add missing info.
    – TomazicM
    Commented Feb 4, 2022 at 12:13
  • Is there any way to crop the polygon? Getting as downloable image. I need the cropped image in base64 format.
    – Omer
    Commented Feb 8, 2022 at 13:24

1 Answer 1

3

Mapbox GL JS is not using 2d context for canvas, but webgl context, so operation is not trivial. Logic goes like this:

  • Polygon coordinates are converted to map pixel coordinates with map.project method.
  • Drawn polygon is temporary deleted so that it does not mix with the clipped image.
  • Get context of map canvas with mapCanvas.getContext('webgl'). For that to work, map option preserveDrawingBuffer has to be set to true.
  • Convert webgl context to 2d context.
  • Actual size of Mapbox GL JS map canvas is bigger than it is shown (resized with style), so calculation of size factors is needed, since point coordinates returned by map.project method are not given for actual canvas, but for resized one.
  • Calculate bounding box for clipped polygon.
  • Translate canvas so that polygon bounding box is at [0, 0].
  • Create new empty canvas for clipped image and draw polygon on it and then clip context to this polygon.
  • Draw map image to this clipped canvas.
  • Create empty final canvas and draw only the clipped polygon to it.
  • Download final canvas as image.

Since I'm not some canvas expert, code is probably not optimal, but it works. Below is relevant part of the code:

const map = new mapboxgl.Map({
  container: 'map', 
  style: 'mapbox://styles/mapbox/satellite-v9', 
  center: [-91.874, 42.76], 
  zoom: 12,
  preserveDrawingBuffer: true
});

function getPixels(ctx) {
  return ctx.readPixels
    ? getPixels3d(ctx)
    : getPixels2d(ctx)
}

function getPixels3d(gl) {
  var canvas = gl.canvas
  var height = canvas.height
  var width  = canvas.width
  var buffer = new Uint8Array(width * height * 4)

  gl.readPixels(0, 0
    , canvas.width
    , canvas.height
    , gl.RGBA
    , gl.UNSIGNED_BYTE
    , buffer
  )

  return buffer
}

function getPixels2d(ctx) {
  var canvas = ctx.canvas
  var height = canvas.height
  var width  = canvas.width

  return ctx.getImageData(0, 0, width, height).data
}

function webglToCanvas2d(webgl, canvas2D) {
  var outCanvas = canvas2D ? canvas2D.canvas || canvas2D : document.createElement('canvas');
  var outContext = outCanvas.getContext('2d');
  var outImageData;

  webgl = webgl instanceof WebGLRenderingContext ? webgl : webgl.getContext('webgl') || webgl.getContext('experimental-webgl');

  outCanvas.width = webgl.canvas.width;
  outCanvas.height = webgl.canvas.height;
  outImageData = outContext.getImageData(0, 0, outCanvas.width, outCanvas.height);

  outImageData.data.set(new Uint8ClampedArray(getPixels3d(webgl).buffer));
  outContext.putImageData(outImageData, 0, 0);
  outContext.translate(0, outCanvas.height);
  outContext.scale(1, -1);
  outContext.drawImage(outCanvas, 0, 0);
  outContext.setTransform(1, 0, 0, 1, 0, 0);

  return outCanvas;
}

function downloadCanvasAsImage(canvas, fileName){
  var link = document.createElement('a');
  link.download = fileName;
  link.href = canvas.toDataURL()
  link.click();
}

var points = [];
var savedPolygon;

function saveClipped() {
  var mapCanvas = map.getCanvas();
  var ctx = mapCanvas.getContext("webgl");
  var canvas2d = webglToCanvas2d(ctx);
  var ctx2d = canvas2d.getContext('2d');
  var clippedCanvas = document.createElement('canvas');
  clippedCanvas.width = mapCanvas.clientWidth;
  clippedCanvas.height = mapCanvas.clientHeight;
  var clippedCtx = clippedCanvas.getContext('2d');
  var factX = mapCanvas.clientWidth / mapCanvas.width;
  var factY = mapCanvas.clientHeight / mapCanvas.height
  var minX = points[0].x;
  var maxX = points[0].x;
  var minY = points[0].y;
  var maxY = points[0].y;
  for (var i = 1; i < points.length; i++) {
    if (points[i].x < minX)
      minX = points[i].x
    else if (points[i].x > maxX) {
      maxX = points[i].x
    }
    if (points[i].y < minY)
      minY = points[i].y
    else if (points[i].y > maxY) {
      maxY = points[i].y
    }
  }
  clippedCtx.scale(1/factX, 1/factY);
  clippedCtx.translate(-minX, -minY);
  clippedCtx.beginPath();
  clippedCtx.moveTo(points[0].x, points[0].y);
  for (var i = 1; i < points.length; i++) {
    clippedCtx.lineTo(points[i].x, points[i].y);
  }
  clippedCtx.closePath();
  clippedCtx.lineWidth = 1;
  clippedCtx.stroke();
  clippedCtx.clip();
  clippedCtx.scale(factX, factY);
  clippedCtx.drawImage(canvas2d, 0, 0);
  var finalCanvas = document.createElement('canvas');
  var finalWidth = (maxX - minX) / factX;
  var finalHeight = (maxY - minY) / factY;
  finalCanvas.width = finalWidth;
  finalCanvas.height = finalHeight;
  var finalCtx = finalCanvas.getContext('2d');
  finalCtx.drawImage(clippedCanvas, 0, 0, finalWidth, finalHeight, 0, 0, finalWidth, finalHeight);
  downloadCanvasAsImage(finalCanvas, 'clipped-image.png');
  draw.add(savedPolygon);
}

function updateArea(e) {
  if (e.features[0].geometry.type == 'Polygon') {
    savedPolygon = e.features[0];
    var polygonId = e.features[0].id;
    var coords = savedPolygon.geometry.coordinates[0];
    coords.forEach(function(coord) {
      points.push(map.project(coord));
    });
    draw.delete(polygonId);
    setTimeout(saveClipped, 100);
  }
  const data = draw.getAll();
  const answer = document.getElementById('calculated-area');
  if (data.features.length > 0) {
    const area = turf.area(data);
    const rounded_area = Math.round(area * 100) / 100;
    answer.innerHTML = `<p><strong>${rounded_area}</strong></p><p>square meters</p>`;
  } else {
    answer.innerHTML = '';
  }
}

Here is an example:

enter image description here

6
  • It's quite explanatory and in depth. When i tested i am getting an empty cropped polygon image(without map) after downloading it. The example image you showed is exactly what i want to achieve.
    – Omer
    Commented Feb 9, 2022 at 10:18
  • 1
    Yes, I forgot to mention that map option preserveDrawingBuffer has to be set to true. See modified answer.
    – TomazicM
    Commented Feb 9, 2022 at 10:33
  • The solution you mentioned works fantastic . Just a last question is it possible to get the cropped polygon image as a tiff format. Becuase image is getting blurred when i am zooming it.
    – Omer
    Commented Feb 10, 2022 at 8:05
  • 1
    Format of image wouldn't change anything, since image quality depends on resolution, and resolution cannot be better than screen resolution at the time of image clipping.
    – TomazicM
    Commented Feb 10, 2022 at 8:37
  • 1
    No, you can't do it directly, but you can use some library for that, like for example github.com/motiz88/canvas-to-tiff.
    – TomazicM
    Commented Feb 12, 2022 at 10:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.