改造leaflet圖片插件Leaflet.ImageOverlay.Rotated,通過圖片矩形對角線上的2個頂點控制圖片的移動、旋轉和縮放,並且保持圖片長寬比不變,圖片不變形。


簡介:公司需要做個地圖圖片編輯功能,將平面圖上傳到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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM