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')
.
- 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:
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: