簡介:公司需要做個地圖圖片編輯功能,將平面圖上傳到Mapbox地圖上,可拖拽、旋轉、縮放圖片至合適的地理位置上。當時發現MapboxGL不好弄,改成了leaflet地圖,leaflet插件很多,找到https://github.com/IvanSanchez/Leaflet.ImageOverlay.Rotated圖片編輯插件,但發現操作無法保證圖片不變形,所以做了些改造,這里順便做個筆記以便日后查閱。
1、Leaflet.ImageOverlay.Rotated插件原理分析:
添加圖片要傳入3個頂點坐標,topleft, topright, bottomleft,也是可以拖拽的三個控制點。
1 var overlay = L.imageOverlay.rotated(imgUrl, topLeftPoint, topRightPoint, bottomLeftPoint, { 2 opacity: 0.9, 3 interactive: true, 4 corners:[], 5 w: null, 6 h: null 7 });
圖片添加到地圖上,會生成一個外接矩形框(默認是看不到的,我加了border使其可見,便於分析),然后你拖拽3個頂點marker, 插件內部通過數學計算會推斷第四個頂點的位置,然后根據4個頂點的位置顯示圖片。
圖(1)
每次拖拽都會調用reposition方法重設圖片位置。
1 reposition: function(topleft, topright, bottomleft) { 2 this._topLeft = L.latLng(topleft); 3 this._topRight = L.latLng(topright); 4 this._bottomLeft = L.latLng(bottomleft); 5 this._reset(); 6 },
重點邏輯在_reset方法中, 每次reset都會根據3個頂點推斷第四個頂點,然后計算外接矩形框。
1 _reset: function () { 2 var div = this._image; 3 4 // Project control points to container-pixel coordinates 5 // 1)先將3個頂點經緯度轉成平面坐標 6 var pxTopLeft = this._map.latLngToLayerPoint(this._topLeft); 7 var pxTopRight = this._map.latLngToLayerPoint(this._topRight); 8 var pxBottomLeft = this._map.latLngToLayerPoint(this._bottomLeft); 9 10 // Infer coordinate of bottom right 11 // 2)然后第4個頂點坐標右下=右上-左上+左下 12 var pxBottomRight = pxTopRight.subtract(pxTopLeft).add(pxBottomLeft); 13 14 // pxBounds is mostly for positioning the <div> container 15 // 3)計算4個頂點的外接矩形,返回的pxBounds由四個頂點中的x、y最值構成,即max和min兩個頂點對象 16 var pxBounds = L.bounds([pxTopLeft, pxTopRight, pxBottomLeft, pxBottomRight]); 17 18 //size是外接矩形的寬高,寬=max.x - min.x, 高=max.y - min.y 19 var size = pxBounds.getSize(); 20 21 //用topLeft減去外接矩形最小頂點坐標得到topLeft相對於該外接矩形的坐標位置 22 var pxTopLeftInDiv = pxTopLeft.subtract(pxBounds.min); 23 24 // Calculate the skew angles, both in X and Y 25 //4)計算斜切角,這里用到了css3的skew樣式,可改變dom結點的傾斜程度,如下圖(2) 26 var vectorX = pxTopRight.subtract(pxTopLeft); 27 var vectorY = pxBottomLeft.subtract(pxTopLeft); 28 var skewX = Math.atan2( vectorX.y, vectorX.x ); 29 var skewY = Math.atan2( vectorY.x, vectorY.y ); 30 31 // LatLngBounds used for animations 32 this._bounds = L.latLngBounds( this._map.layerPointToLatLng(pxBounds.min), 33 this._map.layerPointToLatLng(pxBounds.max) ); 34 35 L.DomUtil.setPosition(div, pxBounds.min); 36 37 div.style.width = size.x + 'px'; 38 div.style.height = size.y + 'px'; 39 40 var imgW = this._rawImage.width; 41 var imgH = this._rawImage.height; 42 if (!imgW || !imgH) { 43 return; // Probably because the image hasn't loaded yet. 44 } 45 46 //5)計算圖片的縮放比例 47 var scaleX = pxTopLeft.distanceTo(pxTopRight) / imgW * Math.cos(skewX); 48 var scaleY = pxTopLeft.distanceTo(pxBottomLeft) / imgH * Math.cos(skewY); 49 50 this._rawImage.style.transformOrigin = '0 0'; 51 52 this._rawImage.style.transform = 53 'translate(' + pxTopLeftInDiv.x + 'px, ' + pxTopLeftInDiv.y + 'px)' + 54 'skew(' + skewY + 'rad, ' + skewX + 'rad) ' + 55 'scale(' + scaleX + ', ' + scaleY + ') '; 56 },
圖(2)
由於圖片完全由3個頂點的位置控制而沒有約束,所以圖片會出現長寬不等、鄰邊夾角變化導致的圖片變形,因此要改造。
2、如何改造,實現圖片不變形,即保證圖片是矩形且長寬比不變,如下圖所示:
圖(3)
圖片ABCD是第一次初始化位置,A和C是2個可拖拽的頂點,每次拖拽只要保證B、D兩個頂點是唯一確定的就能能保證圖片不變形。
向量AB在向量AC上的投影c的長度與向量AC的長度比值是常數,即 k1 = |c| / |AC|, k1是固定不變的。證明如下:
|c| / |AC| = 向量AB · 向量AC / (向量AC * 向量AC) = |AB|*|AC| *cosα/ |AC|^2 = |AB|/ |AC| * cosα = cosα ^2, 因為對固定長寬比例且不變形的矩形圖片角α是不變的,所以|c| / |AC| = k是常數。
同理將向量AC逆時針旋轉90度得到向量AU,向量AB在向量AU上的投影b的長度與向量AU的長度比值也是常數,即k2 = |b| / |AU|, k2是固定不變的。
我們發現向量AB=向量b+向量c, 因此我們每次只需要計算出b,c兩個向量就可以唯一確定向量AB,進而確定了頂點B的位置。
同理,其它的頂點也這樣計算。
計算用到了余弦定理、平面向量數學知識,關鍵代碼貼下:
1)向量的數量積計算:
1 function getDotProduction(point1, point2){ 2 return point1.x * point2.x + point1.y * point2.y; 3 }
2)向量順時針旋轉90度:
1 function getClockWiseRotate90DegreePoint(point){ 2 return L.point([point.y, -point.x]); 3 }
3)計算新頂點的位置:
1 function getCornerLatLng(point, bottomLeftMarkerLatLng, topRightMarkerLatLng){ 2 var boundsRectBottomLeft= L.point(0, 0);// origin 3 var boundsRectTopRight= L.point(imgWidth, imgHeight); 4 var diagonalVector = boundsRectTopRight.subtract(boundsRectBottomLeft); 5 var pV = point.subtract(boundsRectBottomLeft); 6 var rotate90V = getClockWiseRotate90DegreePoint(diagonalVector); 7 var scaleX = getDotProduction(diagonalVector, pV) / getDotProduction(diagonalVector, diagonalVector); 8 var scaleY = -getDotProduction(rotate90V, pV) / getDotProduction(rotate90V, rotate90V); 9 var bLMarkerPx = L.Projection.SphericalMercator.project(bottomLeftMarkerLatLng); 10 var tRMarkerPx = L.Projection.SphericalMercator.project(topRightMarkerLatLng); 11 var vx = bLMarkerPx.add(tRMarkerPx.subtract(bLMarkerPx).multiplyBy(scaleX)); 12 var vy = getClockWiseRotate90DegreePoint(bLMarkerPx.subtract(tRMarkerPx)).multiplyBy(scaleY); 13 var p = vx.add(vy); 14 return L.Projection.SphericalMercator.unproject(p); 15 }
4)拖拽事件及reset方法:topRightMarker.on('drag dragend', repositionImage);
bottomLeftMarker.on('drag dragend', repositionImage); function repositionImage() { var tRlnglat = topRightMarker.getLatLng(); var bLlnglat = bottomLeftMarker.getLatLng(); var imgWidth = overlay.options.w; var imgHeight = overlay.options.h; var c1 = getCornerLatLng(L.point(0, imgHeight), bLlnglat, tRlnglat); // raw image topleft var c2 = getCornerLatLng(L.point(imgWidth, imgHeight), bLlnglat, tRlnglat); // raw image topright var c3 = getCornerLatLng(L.point(0, 0), bLlnglat, tRlnglat); // raw image bottomLeft var c4 = getCornerLatLng(L.point(imgWidth, 0), bLlnglat, tRlnglat); overlay.options.corners = [c1, c2, c3, c4];
overlay.reposition(c1, c2, c3); }
實現效果:
項目地址:https://github.com/wxzen/Leaflet.ImageOverlay.Rotated-by-Two-Markers