mapbox-gl-js:调整可见区域&对于给定的线,轴承到给定的线距

时间:2017-05-01 02:20:57

标签: javascript mapbox mapbox-gl-js turfjs

我正在尝试优化长途远足径的Mapbox视图,例如Appalachian Trail或Pacific Crest Trail。这是一个例子,我手工定向,展示了西班牙的SendaPirenáica:

screen capture

给出了感兴趣的区域,视口和音高。我需要找到正确的中心,方位和缩放。

map.fitBounds方法对我没有帮助,因为它假设pitch = 0且bearing = 0。

我已经做了一些探讨,这似乎是smallest surrounding rectangle问题的一个变种,但我还是坚持了几个额外的并发症:

  1. 如何解释音高的扭曲效应?
  2. 如何优化视口的宽高比?请注意,将视口缩小或更宽会改变最佳解决方案的影响:
  3. sketch

    FWIW我也在使用turf-js,这有助于我获得该线的凸包。

3 个答案:

答案 0 :(得分:8)

此解决方案导致在正确方位上显示的路径具有品红色梯形轮廓,显示目标“最紧密的梯形”以显示计算结果。来自顶角的额外行显示了map.center()值的位置。

方法如下:

  1. 使用“fitbounds”技术渲染地图的路径,以获得“北向上和俯仰= 0”情况的近似缩放级别
  2. 将音高旋转到所需的角度
  3. 从画布上抓住梯形
  4. 此结果如下所示:

    Initial view trapezoid

    在此之后,我们想要在路径周围旋转该梯形,并找到梯形与点之间最紧密的拟合。为了测试最紧密的配合,更容易旋转路径而不是梯形,所以我在这里采取了这种方法。我没有在路径上实现“凸包”以最小化旋转的点数,但这可以作为优化步骤添加。
    为了获得最紧密的匹配,第一步是移动map.center(),使路径位于视图的“后面”。这是最大的空间在平截头体中的位置,因此在那里操作它很容易:

    The yellow shows the adjusted view position, putting the path at the back of the view

    接下来,我们测量成角度的梯形墙与路径中的每个点之间的距离,从而节省左侧和右侧的最近点。然后,我们通过基于这些距离水平平移视图来居中视图中的路径,然后缩放视图以消除两侧的空间,如下面的绿色梯形所示:

    Green trapezoid shows the smallest fit

    用于获得“最紧密贴合”的尺度给出了我们对这是否是路径的最佳视图的排名。但是,这个视图可能不是最好的视觉效果,因为我们将路径推到视图的后面以确定排名。相反,我们现在调整视图以将路径放置在视图的垂直中心,并相应地缩放视图三角形。这为我们提供了洋红色的“最终”视图:

    Final view in magenta.

    最后,这个过程是针对每个度数完成的,最小比例值决定了获胜方位,我们从那里获取相关的比例和中心位置。

    mapboxgl.accessToken = 'pk.eyJ1IjoiZm1hY2RlZSIsImEiOiJjajJlNWMxenowNXU2MzNudmkzMndwaGI3In0.ALOYWlvpYXnlcH6sCR9MJg';
    
    var map;
    
    var myPath = [
            [-122.48369693756104, 37.83381888486939],
            [-122.48348236083984, 37.83317489144141],
            [-122.48339653015138, 37.83270036637107],
            [-122.48356819152832, 37.832056363179625],
            [-122.48404026031496, 37.83114119107971],
            [-122.48404026031496, 37.83049717427869],
            [-122.48348236083984, 37.829920943955045],
            [-122.48356819152832, 37.82954808664175],
            [-122.48507022857666, 37.82944639795659],
            [-122.48610019683838, 37.82880236636284],
            [-122.48695850372314, 37.82931081282506],
            [-122.48700141906738, 37.83080223556934],
            [-122.48751640319824, 37.83168351665737],
            [-122.48803138732912, 37.832158048267786],
            [-122.48888969421387, 37.83297152392784],
            [-122.48987674713133, 37.83263257682617],
            [-122.49043464660643, 37.832937629287755],
            [-122.49125003814696, 37.832429207817725],
            [-122.49163627624512, 37.832564787218985],
            [-122.49223709106445, 37.83337825839438],
            [-122.49378204345702, 37.83368330777276]
        ];
    
    var myPath2 = [
            [-122.48369693756104, 37.83381888486939],
            [-122.49378204345702, 37.83368330777276]
        ];
    
    function addLayerToMap(name, points, color, width) {
        map.addLayer({
            "id": name,
            "type": "line",
            "source": {
                "type": "geojson",
                "data": {
                    "type": "Feature",
                    "properties": {},
                    "geometry": {
                        "type": "LineString",
                        "coordinates": points
                    }
                }
            },
            "layout": {
                "line-join": "round",
                "line-cap": "round"
            },
            "paint": {
                "line-color": color,
                "line-width": width
            }
        });
    }
    function Mercator2ll(mercX, mercY) { 
        var rMajor = 6378137; //Equatorial Radius, WGS84
        var shift  = Math.PI * rMajor;
        var lon    = mercX / shift * 180.0;
        var lat    = mercY / shift * 180.0;
        lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0);
    
        return [ lon, lat ];
    }
    
    function ll2Mercator(lon, lat) {
        var rMajor = 6378137; //Equatorial Radius, WGS84
        var shift  = Math.PI * rMajor;
        var x      = lon * shift / 180;
        var y      = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
        y = y * shift / 180;
    
        return [ x, y ];
    }
    
    function convertLL2Mercator(points) {
        var m_points = [];
        for(var i=0;i<points.length;i++) {
            m_points[i] = ll2Mercator( points[i][0], points[i][1] );
        }
        return m_points;
    }
    function convertMercator2LL(m_points) {
        var points = [];
        for(var i=0;i<m_points.length;i++) {
            points[i] = Mercator2ll( m_points[i][0], m_points[i][1] );;
        }
        return points;
    }
    function pointsTranslate(points,xoff,yoff) {
        var newpoints = [];
        for(var i=0;i<points.length;i++) {
            newpoints[i] = [ points[i][0] + xoff, points[i][1] + yoff ];
        }
        return(newpoints);
    }
    
    // note [0] elements are lng [1] are lat
    function getBoundingBox(arr) {
        var ne = [ arr[0][0] , arr[0][1] ]; 
        var sw = [ arr[0][0] , arr[0][1] ]; 
        for(var i=1;i<arr.length;i++) {
            if(ne[0] < arr[i][0]) ne[0] = arr[i][0];
            if(ne[1] < arr[i][1]) ne[1] = arr[i][1];
            if(sw[0] > arr[i][0]) sw[0] = arr[i][0];
            if(sw[1] > arr[i][1]) sw[1] = arr[i][1];
        }
        return( [ sw, ne ] );
    }
    
    function pointsRotate(points, cx, cy, angle){
        var radians = angle * Math.PI / 180.0;
        var cos = Math.cos(radians);
        var sin = Math.sin(radians);
        var newpoints = [];
    
        function rotate(x, y) {
            var nx = cx + (cos * (x - cx)) + (-sin * (y - cy));
            var ny = cy + (cos * (y - cy)) + (sin * (x - cx));
            return [nx, ny];
        }
        for(var i=0;i<points.length;i++) {
            newpoints[i] = rotate(points[i][0],points[i][1]);
        }
        return(newpoints);
    }
    
    function convertTrapezoidToPath(trap) {
        return([ 
            [trap.Tl.lng, trap.Tl.lat], [trap.Tr.lng, trap.Tr.lat], 
            [trap.Br.lng, trap.Br.lat], [trap.Bl.lng, trap.Bl.lat], 
            [trap.Tl.lng, trap.Tl.lat] ]);
    }
    
    function getViewTrapezoid() {
        var canvas = map.getCanvas();
        var trap = {};
    
        trap.Tl = map.unproject([0,0]);
        trap.Tr = map.unproject([canvas.offsetWidth,0]);
        trap.Br = map.unproject([canvas.offsetWidth,canvas.offsetHeight]);
        trap.Bl = map.unproject([0,canvas.offsetHeight]);
    
        return(trap);
    }
    
    function pointsScale(points,cx,cy, scale) {
        var newpoints = []
    
        for(var i=0;i<points.length;i++) {
            newpoints[i] = [ cx + (points[i][0]-cx)*scale, cy + (points[i][1]-cy)*scale ];
        }
        return(newpoints);
    }
    
    var id = 1000;
    function convertMercator2LLAndDraw(m_points, color, thickness) {
        var newpoints = convertMercator2LL(m_points);
        addLayerToMap("id"+id++, newpoints, color, thickness);
    }
    
    function pointsInTrapezoid(points,yt,yb,xtl,xtr,xbl,xbr) {
        var str = "";
        var xleft = xtr;
        var xright = xtl;
    
        var yh = yt-yb;
        var sloperight = (xtr-xbr)/yh;
        var slopeleft = (xbl-xtl)/yh;
    
        var flag = true;
    
        var leftdiff = xtr - xtl;
        var rightdiff = xtl - xtr;
    
        var tmp = [ [xtl, yt], [xtr, yt], [xbr,yb], [xbl,yb], [xtl,yt] ];
    //    convertMercator2LLAndDraw(tmp, '#ff0', 2);
    
        function pointInTrapezoid(x,y) {
            var xsloperight = xbr + sloperight * (y-yb);
            var xslopeleft = xbl - slopeleft * (y-yb);
    
            if((x - xsloperight) > rightdiff) {
                rightdiff = x - xsloperight;
                xright = x;
            }
            if((x - xslopeleft) < leftdiff) {
                leftdiff = x - xslopeleft;
                xleft = x;
            }
    
            if( (y<yb) || (y > yt) ) {
                console.log("y issue");
            }
            else if(xsloperight < x) {
                console.log("sloperight");
            }
            else if(xslopeleft > x) {
                console.log("slopeleft");
            } 
            else return(true);
            return(false);
        }
    
        for(var i=0;i<points.length;i++) {
            if(pointInTrapezoid(points[i][0],points[i][1])) {
                str += "1";
            }
            else {
                str += "0";
                flag = false;
            }
        }
        if(flag == false) console.log(str);
    
        return({ leftdiff: leftdiff, rightdiff: rightdiff });
    }
    
    var viewcnt = 0;
    function calculateView(trap, points, center) {
        var bbox = getBoundingBox(points);
        var bbox_height = Math.abs(bbox[0][1] - bbox[1][1]);
        var view = {};
    
        // move the view trapezoid so the path is at the far edge of the view
        var viewTop = trap[0][1];
        var pointsTop = bbox[1][1];
        var yoff = -(viewTop - pointsTop); 
    
        var extents = pointsInTrapezoid(points,trap[0][1]+yoff,trap[3][1]+yoff,trap[0][0],trap[1][0],trap[3][0],trap[2][0]);
    
        // center the view trapezoid horizontally around the path
        var mid = (extents.leftdiff - extents.rightdiff) / 2;
    
        var trap2 = pointsTranslate(trap,extents.leftdiff-mid,yoff);
    
        view.cx = trap2[5][0];
        view.cy = trap2[5][1];
    
        var w = trap[1][0] - trap[0][0];
        var h = trap[1][1] - trap[3][1];
    
        // calculate the scale to fit the trapezoid to the path
        view.scale = (w-mid*2)/w;
    
        if(bbox_height > h*view.scale) {
            // if the path is taller than the trapezoid then we need to make it larger
            view.scale = bbox_height / h;
        }
        view.ranking = view.scale;
    
        var trap3 = pointsScale(trap2,(trap2[0][0]+trap2[1][0])/2,trap2[0][1],view.scale);
    
        w = trap3[1][0] - trap3[0][0];
        h = trap3[1][1] - trap3[3][1];
        view.cx = trap3[5][0];
        view.cy = trap3[5][1];
    
        // if the path is not as tall as the view then we should center it vertically for the best looking result
        // this involves both a scale and a translate
        if(h > bbox_height) {
            var space = h - bbox_height;
            var scale_mul = (h+space)/h;
            view.scale = scale_mul * view.scale;
            cy_offset = space/2;
                
            trap3 = pointsScale(trap3,view.cx,view.cy,scale_mul);      
            trap3 = pointsTranslate(trap3,0,cy_offset);
            view.cy = trap3[5][1];
        }
    
        return(view);
    }
    
    function thenCalculateOptimalView(path) {
        var center = map.getCenter();
        var trapezoid = getViewTrapezoid();
        var trapezoid_path = convertTrapezoidToPath(trapezoid);
        trapezoid_path[5] = [center.lng, center.lat];
    
        var view = {};
        //addLayerToMap("start", trapezoid_path, '#00F', 2);
    
        // get the mercator versions of the points so that we can use them for rotations
        var m_center = ll2Mercator(center.lng,center.lat);
        var m_path = convertLL2Mercator(path);
        var m_trapezoid_path = convertLL2Mercator(trapezoid_path);
    
        // try all angles to see which fits best
        for(var angle=0;angle<360;angle+=1) {
            var m_newpoints = pointsRotate(m_path, m_center[0], m_center[1], angle);
            var thisview = calculateView(m_trapezoid_path, m_newpoints, m_center);
            if(!view.hasOwnProperty('ranking') || (view.ranking > thisview.ranking)) {           
                view.scale = thisview.scale;
                view.cx = thisview.cx;
                view.cy = thisview.cy;
                view.angle = angle;
                view.ranking = thisview.ranking;
            }
        }
    
        // need the distance for the (cx, cy) from the current north up position
        var cx_offset = view.cx - m_center[0]; 
        var cy_offset = view.cy - m_center[1];
        var rotated_offset =  pointsRotate([[cx_offset,cy_offset]],0,0,-view.angle);
    
        map.flyTo({ bearing: view.angle, speed:0.00001 });
    
        // once bearing is set, adjust to tightest fit
        waitForMapMoveCompletion(function () {
            var center2 = map.getCenter();
            var m_center2 = ll2Mercator(center2.lng,center2.lat);
            m_center2[0] += rotated_offset[0][0];        
            m_center2[1] += rotated_offset[0][1];
            var ll_center2 = Mercator2ll(m_center2[0],m_center2[1]);
            map.easeTo({
                center:[ll_center2[0],ll_center2[1]], 
                zoom : map.getZoom() });
            console.log("bearing:"+view.angle+ " scale:"+view.scale+" center: ("+ll_center2[0]+","+ll_center2[1]+")");
    
            // draw the tight fitting trapezoid for reference purposes    
            var m_trapR = pointsRotate(m_trapezoid_path,m_center[0],m_center[1],-view.angle);
            var m_trapRS = pointsScale(m_trapR,m_center[0],m_center[1],view.scale);
            var m_trapRST = pointsTranslate(m_trapRS,m_center2[0]-m_center[0],m_center2[1]-m_center[1]);
            convertMercator2LLAndDraw(m_trapRST,'#f0f',4);
        });
    }
    
    function waitForMapMoveCompletion(func) {
        if(map.isMoving()) 
            setTimeout(function() { waitForMapMoveCompletion(func); },250);
        else
            func();
    }
    
    function thenSetPitch(path,pitch) {
        map.flyTo({ pitch:pitch } );
        waitForMapMoveCompletion(function() { thenCalculateOptimalView(path); })
    }
    
    function displayFittedView(path,pitch) {
        var bbox = getBoundingBox(path);
        var path_cx = (bbox[0][0]+bbox[1][0])/2;
        var path_cy = (bbox[0][1]+bbox[1][1])/2;
    
        // start with a 'north up' view
        map = new mapboxgl.Map({
            container: 'map',
            style: 'mapbox://styles/mapbox/streets-v9',
            center: [path_cx, path_cy],
            zoom: 12
        });
    
        // use the bounding box to get into the right zoom range
        map.on('load', function () {
            addLayerToMap("path",path,'#888',8);
            map.fitBounds(bbox);
            waitForMapMoveCompletion(function() { thenSetPitch(path,pitch); });
        });
    }
    
    window.onload = function(e) {
        displayFittedView(myPath,60);
    }
    body { margin:0; padding:0; }
    #map { position:absolute; top:0; bottom:0; width:100%; }
    <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.js'></script>
    <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.css' rel='stylesheet' />
    <div id='map'></div>

答案 1 :(得分:2)

最小的周围矩形将特定于pitch = 0(直接向下看)。

一种选择是继续使用最小的周围矩形方法并计算目标区域的变换 - 就像3d引擎一样。 如果您这样做,可以浏览unity docs以更好地理解viewing frustum 的机制

我觉得这不适合你的问题,因为你必须从不同的角度重新计算目标区域的2d渲染,这是一种相对昂贵的蛮力。

规范化计算的另一种方法是将视口投影渲染到目标区域平面。亲眼看看:

rough projection

然后你所要做的就是“只是”找出原始凸壳可以装入该形状的梯形的最大尺寸(特别是convex isosceles trapezoid因为我们不操纵相机滚动)。

这是我深入了解的地方,不知道在哪里指点计算。我认为在这个2D空间中迭代可能的解决方案至少会更便宜。

P.S:要记住的另一件事是视口投影形状将根据FOV(视野)而不同。

当您调整浏览器视口的大小时,会发生这种情况,但mapbox-gl-js中的属性为doesn't seem to be exposed

修改

经过一番思考后,我觉得最好的数学解决方案在现实中会感觉有点“干”。不是跨越用例,并且可能做出一些错误的假设,我会问这些问题:

  • 对于大致是直线的路线,它是否总是被平移,所以末端位于左下角和右上角?这将接近“最佳”,但可能会......无聊。
  • 您是否希望将更多路径保持在靠近视口的位置?如果大部分距离视口很远,您可能会丢失路线详细信息。
  • 您会选择关注点吗?那些可能更接近视口。

根据船体形状对不同类型的路线进行分类并创建平移预设可能会很方便吗?

答案 2 :(得分:0)

希望这可以通过一些调整为您指明正确的方向。

首先,我设置了我们要展示的两个点

function middleCoord(a, b){
  let x = (a - b)/2
  return _.min([a, b]) + x
}
let center = [middleCoord(pointA[0], pointB[0]), middleCoord(pointA[1], pointB[1])]

然后我找到了这两点的中间部分。我为此做了我自己的功能,但看起来草皮可以做到这一点。

let p1 = turf.point(pointA)
let p2 = turf.point(pointB)
let points = turf.featureCollection([p1, p2])
let bearing = turf.bearing(p2, p1)

我使用turfs轴承功能从第二点看第一点的视图

var map = new mapboxgl.Map({
  container: 'map', // container id
  style: 'mapbox://styles/mapbox/outdoors-v10', //hosted style id
  center: center, // starting position
  zoom: 4, // starting zoom
  pitch: 60,
  bearing: bearing
})

map.fitBounds([pointA, pointB], {padding: 0, offset: 0})

然后我调用地图并运行fitBounds函数:

{{1}}

这是一个codepen:https://codepen.io/thejoshderocher/pen/BRYGXq

要调整轴承以最佳使用,屏幕尺寸是为了获得窗口的大小并调整轴承以充分利用可用的屏幕空间。如果它是一个纵向移动屏幕,这个轴承工作完美。如果您在具有宽视图的桌面上,则需要旋转,以使A点位于其中一个顶角。