leaflet测绘功能,实现下载测绘结果图
做一个平面地图的测绘功能,可在测绘图片上设定比例尺、测距,计算面积、规划区域等,并将最终测绘结果下载为png图片(原图)。
项目地址:https://github.com/kikiy7/leaflet-image-draw
一、leaflet创建绘图工具
基于:leaflet-image-hotspots 实现热点图像区域编辑功能,能够在图片上绘制、编辑、删除图形区域以及文字说明。
API 参考:单张图纸(主要的绘图API)
1.创建地图
map = new L.Map(domId, { editable: true, crs: L.CRS.Simple, //注:若不了解该属性请勿随意更改 maxZoom : 6, minZoom : -2, center : [ $('#'+domId).width() / 2, $('#'+domId).height() / 2 ] });
2.创建绘图工具
var drawControl = new L.Control.Draw({ position: 'topright', draw: { polyline: { showLength:true, metric:true }, polygon: { allowIntersection: false, // Restricts shapes to simple polygons showArea: true, drawError: { color: '#e1e100', // Color the shape will turn when intersects message: '<strong>错误<strong>,你不能这么画!' }, shapeOptions: { weight : weight, } }, rectangle: { shapeOptions: { weight : weight } }, circle:false, //关闭画圆功能 marker:false //关闭标记功能 }, edit: { featureGroup: drawnItems, poly: { allowIntersection: false }, remove: true }
}); //将绘图工具添加到地图上 if(map.addControl(drawControl)){ callback(); }
二、 面积及路径的计算
1.面积计算公式
一开始是使用leaflet自带的 L.GeometryUtil.geodesicArea() 计算面积方法,和 L.latLng().distanceTo() 计算两点间的距离的方法,发现当在地图模式为 crs: L.CRS.Simple, 也就是本案例中所用的地图模式,其leaflet自带的经纬度面积的算法不适用于该平面测绘,所以另外找了一个计算任意多边形面积的方法。
计算长度:
function computeLength(line){ let dis = 0; for (let i = 0; i <line.length - 1;i++){ let start = line[i]; let end = line[i+1]; let dx = start.lng - end.lng; let dy = start.lat -end.lat; dis += Math.sqrt(dx*dx + dy*dy); } return dis; }
计算面积:
function computePolygonArea(lnglats) { let length = lnglats.length; let s = lnglats[0].lng * (lnglats[length - 1].lat - lnglats[1].lat); for (let i = 1;i < length; i++){ s += lnglats[i].lng * (lnglats[i-1].lat - lnglats[(i+1) % length].lat); } return Math.abs(s/2);
}
由于本案例是直接采用L.Control.Draw 集成绘图工具控制器,监听绘图创建事件,只需要在绘制某图形结束时获取 layer.getLatLngs() 并传入方法即可。
//监听绘图工具创建图形事件 map.on(L.Draw.Event.CREATED, function (e) { var type = e.layerType, layer = e.layer; //drawnItems.addLayer(layer); layer.addTo(drawnItems); console.log(scale_length) if (scale_length == 0){ layer.setText("请先设置比例尺"); }else { if (type === 'marker') { swal({ title: '标记', input: 'text', showCancelButton: true, inputValidator: function (value) { return new Promise(function (resolve, reject) { if (value) { resolve(); } else { reject('你得填一下你标记了啥!'); } }) } }).then(function (result) { pointAttr.push({ 'leaflet_id': layer._leaflet_id, 'type': type }); layer.bindLabel(result); swal({ type: 'success', html: '您标记了:' + result }); }, function (dismiss) { drawnItems.removeLayer(layer); }); } else { let latlng = layer.getLatLngs(); if (type === "polyline") { let distance = (computeLength(latlng) / scale_length * scale).toFixed(2); layer.setText(distance + "m"); } else { let area = (computePolygonArea(latlng) / scale_length / scale_length * scale * scale).toFixed(2); layer.setText(area + "m²"); } } } swal({ title: '绘制成功', showCancelButton: true, }).then(function (result) { pointAttr.push({ 'leaflet_id': layer._leaflet_id, 'type': type }); }, function (dismiss) { drawnItems.removeLayer(layer); }); }); //监听绘图工具编辑图形事件 map.on(L.Draw.Event.EDITED, function (e) { var layers = e.layers; var countOfEditedLayers = 0; layers.eachLayer(function (layer) { resetText(layer); countOfEditedLayers++; }); console.log("修改了 " + countOfEditedLayers + " 个图层"); });
2.设置比例尺
点击比例尺时激活地图点击事件
function createScale(e) { e.stopPropagation(); let sLen = scale_geometry.length; if(sLen>0){ for (let i = 0 ; i<sLen; i++){ map.removeLayer(scale_geometry[i]); } } scale_line = L.polyline(scaleArr,{color:'#000000'}); map.on("click",addScale); }
添加点,存入scale_geometry数组以便在重设比例尺时移除地图上的比例尺图形。
function addScale(e){ scaleArr.push([e.latlng.lat, e.latlng.lng]); scale_line.addLatLng(e.latlng); map.addLayer(scale_line); const node=new L.circleMarker(e.latlng , { color: '#363333', fillColor: '#363030', fillOpacity: 1 ,radius:5 }); map.addLayer(node); scale_geometry.push(node); map.on('mousemove', scale_lineMove);//双击地图 if (scaleArr.length == 2){ scale_end(); } }
鼠标移动跟随线段
function scale_lineMove(e){ if (scaleArr.length > 0) { let ls = [scaleArr[scaleArr.length - 1], [e.latlng.lat, e.latlng.lng]] tempLine.setLatLngs(ls); map.addLayer(tempLine); } }
结束比例尺绘制并弹出设置框
function scale_end(e){ scaleArr = []; scale_geometry.push(scale_line); map.removeLayer(tempLine); map.off('mousemove'); map.off("click",addScale); //弹出设置框 swal({ title: '请输入正整数(单位为米)', input: 'text', showCancelButton: true, inputValidator: function (value) { return new Promise(function (resolve, reject) { let r = /^\+?[1-9][0-9]*$/; if (r.test(value)) { resolve(); scale=parseInt(value); scale_length = computeLength(scale_line.getLatLngs()); } else { reject('无效输入,请输入正整数!'); } }) } }).then(function (result) { scale_line.bindLabel('比例尺'); scale_line.setText(scale+"m"); $.each(drawnItems._layers,function (i,layer) { resetText(layer); }) swal({ type: 'success', html: '比例尺设置成功。' }) }, function (dismiss) { for (let i = 0 ; i<scale_geometry.length; i++){ map.removeLayer(scale_geometry[i]); } }); }
重新绘制图形上的文本(显示面积于图形上)
function resetText(layer){ let id = layer._leaflet_id; let type; let latlng = layer.getLatLngs(); if (scale_length == 0){ return; } $.each(pointAttr,function (i,item) { if (item.leaflet_id == id){ type = item.type; return false; } }); if (type == "polyline"){ let distance = (computeLength(latlng) / scale_length * scale).toFixed(2); layer.setText(distance+"m"); }else{ let area = (computePolygonArea(latlng)/scale_length/scale_length*scale*scale).toFixed(2); layer.setText(area +"m²"); } }
三、下载原图及其测绘结果
由于leaflet绘图后以svg的形式展现,所以下载图片涉及到canvas转为图片、svg转为图片。
1.调整地图视野级别
leaflet绘图产生的svg图层随着地图缩放而改变矢量图形大小,可以在DOM中查看。将地图缩放到合适的视野级别,显示完整的图片图层,以便截取居于图像上的svg图层。
function download(){ map.fitBounds(map_bound); //恢复整个图像可视范围,为了裁取居于图像上的svg图层 setTimeout(initCanvasData,500); }
2.绘制canvas
先获取地图底图img图片,将canvas的宽高设为其原图的宽高,之后将截取的svg图层叠加绘制于该原图上。
用到的工具:saveSvgAsPng.js
function initCanvasData() { let svgHtml = $('svg.leaflet-zoom-animated')[0]; //获取img图层数据,以便canvas剪切这块区域的svg let map_img = $("img.leaflet-image-layer.leaflet-zoom-animated"); let map_img_width = map_img.width(); let map_img_height = map_img.height(); //获取svg图层数据 let svgContainer = $("svg.leaflet-zoom-animated"); let mapContainer = $("#image-map"); let offset_x = svgContainer.width() - mapContainer.width(); let offset_y = svgContainer.height() - mapContainer.height(); console.log(offset_x,offset_y); //这里svg图层比map容器多了宽和高,下面context.drawImage时减去,否则会有偏移量 let left = map_img.offset().left - svgContainer.offset().left, top = map_img.offset().top - svgContainer.offset().top; //调用方法转换即可,转换结果就是uri, svgAsPngUri(svgHtml, null, function(uri) { let svg = new Image(); svg.src=uri; let image = new Image(); image.src = map_img[0].src; svg.onload = function(){ if(image.complete) { let canvas = drawCanvas(image,svg,left-offset_x,top-offset_y,map_img_width,map_img_height); //下载 downloadCanvas(canvas); }else{ $(image).bind('load',function(){ let canvas = drawCanvas(image,svg,left-offset_x,top-offset_y,map_img_width,map_img_height); //下载 downloadCanvas(canvas); }).bind('error',function(){ //图片加载错误,加入错误处理 // dfd.resolve(); }) } } }); } function drawCanvas(image,svg,location_x,location_y,svg_width,svg_height){ // let canvas = document.createElement('canvas'); //准备空画布 let canvas = document.getElementById("downloadCanvas"); //准备空画布 let context = canvas.getContext('2d'); //取得画布的2d绘图上下文 canvas.width = image.width; canvas.height = image.height; context.drawImage(image,0,0); context.globalCompositeOperation="source-over"; context.drawImage(svg,location_x,location_y,svg_width,svg_height,0,0,image.width,image.height); return canvas; }
3.下载绘制后的canvas
function downloadCanvas(canvas){ var link = document.createElement("a"); var imgData =canvas.toDataURL({format: 'png', multiplier: 4}); var strDataURI = imgData.substr(22, imgData.length); var blob = this.dataURLtoBlob(imgData); var objurl = URL.createObjectURL(blob); link.download = this.cName+".png"; link.href = objurl; link.click(); } //解决原图过大时下载失败问题 function dataURLtoBlob(dataurl) { var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while(n--){ u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], {type:mime}); }