zoukankan      html  css  js  c++  java
  • 改造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

  • 相关阅读:
    数字黑洞
    剪刀石头布
    A除以B
    【Java3】打印三角形
    Servlet 之 javax.servlet 包
    关键字之Super
    设计模式之单例模式
    Servlet 之 读取读取 HTTP 头
    static之类方法和实例方法的区别
    Lambda之通过“方法引用”让你的Lambda表达式更加简洁
  • 原文地址:https://www.cnblogs.com/davidxu/p/10362996.html
Copyright © 2011-2022 走看看