mapbox-gl-js:针对给定的间距,将可见区域和方位调整到给定的线

Posted

技术标签:

【中文标题】mapbox-gl-js:针对给定的间距,将可见区域和方位调整到给定的线【英文标题】:mapbox-gl-js: Adjust visible area & bearing to a given line, for a given pitch 【发布时间】:2017-09-28 12:51:04 【问题描述】:

我正在尝试针对阿巴拉契亚小径或太平洋山脊小径等长途远足小径优化 Mapbox 视图。下面是一个示例,我已经手动确定了方向,展示了西班牙的 Senda Pirenáica:

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

map.fitBounds 方法在这里对我没有帮助,因为它假定俯仰=0 和方位=0。

我做了一些探索,这似乎是 smallest surrounding rectangle 问题的一个变体,但我遇到了一些额外的复杂问题:

    如何解释音高的失真效果? 如何优化视口的纵横比?请注意,使视口变窄或变宽会改变最佳解决方案的方位:

FWIW 我也在使用 turf-js,它可以帮助我获得线的凸包。

【问题讨论】:

因此,您是在询问有关在给定一组参数的情况下计算最小边界框的启发式方法的建议吗?您最好在 GIS.stackexchange 上提问?你到底给了什么?即,您是否选择了间距、屏幕区域和感兴趣区域,然后想要计算相机目标、方位和缩放? 正确 - 给出了间距、视口和路径;我需要中心、方位和变焦。 (我确实检查了 GIS.stackexchange;所以有更多的 Mapbox 活动。)谢谢! 我已经澄清了问题中给出了哪些参数。 我不确定这是否真的是一个 Mapbox 问题——现在可能是一个一般的数学问题。 【参考方案1】:

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

方法如下:

    使用“fitbounds”技术渲染地图的路径,以获得“north up and pitch=0”情况的近似缩放级别 将俯仰角旋转到所需的角度 从画布上抓取梯形

此结果如下所示:

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

接下来,我们测量倾斜的梯形墙与路径中每个点之间的距离,同时保存左右两侧最近的点。然后,我们通过根据这些距离水平平移视图来使路径居中,然后缩放视图以消除两侧的空间,如下面的绿色梯形所示:

用于获得这种“最合适”的比例为我们提供了这是否是路径的最佳视图的排名。然而,这个视图可能不是最好的视觉效果,因为我们将路径推到视图的后面来确定排名。相反,我们现在调整视图以将路径放置在视图的垂直中心,并相应地放大视图三角形。这为我们提供了所需的洋红色“最终”视图:

最后,这个过程对每个度数进行,最小刻度值决定获胜方位,我们从那里获取相关的刻度和中心位置。

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>

【讨论】:

我不认为我遵循梯形拟合的选择方式,你最终会默认使用最后一个合适的排列吗? imgur.com/a/39pg8 看起来更适合旋转 180 度 @o.v.该示例中的“最前”点没有视锥体的前缘那么宽,因此该视图的比例因子相同并且翻转了 180 度。它返回在这种情况下首先找到的那个。如果您将宽度缩小到起点和终点比前缘宽的点,那么它将按照您的预期翻转。 啊,很好,我知道我应该相信数学 @fmacdee - 非常强大的工作,以及精美的答案。谢谢。【参考方案2】:

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

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

 let pointA = [-70, 43]
 let pointB = [-83, 32]

然后我找到了这两点的中间。我为此制作了自己的函数,但看起来 turf 可以做到这一点。

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)

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

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)

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

调整方位以最好地利用屏幕大小是获取窗口的大小并调整方位以最大程度地利用可用的屏幕空间。如果是竖屏的手机屏幕,这个轴承就完美了。如果您在视野开阔的桌面上,则需要旋转以使 A 点位于顶角之一。

【讨论】:

【参考方案3】:

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

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

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

另一种标准化计算的方法是将视口投影渲染到目标区域平面中。自己看:

那么您所要做的就是“只是”找出原始凸包可以放入该形状的梯形的最大尺寸(特别是 convex isosceles trapezoid,因为我们不操纵相机胶卷)。

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

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

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

编辑:

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

对于大致为直线的路线,是否总是将其平移以使末端位于左下角和右上角?这将接近“最佳”,但可能会变得……无聊。 您是否想让更多的路径靠近视口?如果大部分路线远离视口,您可能会丢失路线细节。 您会选择关注点吗?这些可能更靠近视口。

也许按船体形状对不同类型的路线进行分类并创建平移预设会很方便?

【讨论】:

以上是关于mapbox-gl-js:针对给定的间距,将可见区域和方位调整到给定的线的主要内容,如果未能解决你的问题,请参考以下文章

在mapbox-gl-js中居中文本标签?

mapbox-gl-js 围绕 lat/lng 创建一个扇区?

如何在 mapbox-gl-js 中以某些缩放级别隐藏点标签?

如何使用最新的 Mapbox-gl-js 版本避免透明度重叠?

在 Mapbox-GL-JS 中 minzoom 和 maxzoom 到底做了啥?

如何在 mapbox-gl-js 中为源指定授权标头?