简体   繁体   English

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

[英]mapbox-gl-js: Adjust visible area & bearing to a given line, for a given pitch

I'm trying to optimize a Mapbox view for long-distance hiking trails, like the Appalachian Trail or the Pacific Crest Trail. 我正在尝试优化长途远足径的Mapbox视图,例如Appalachian Trail或Pacific Crest Trail。 Here's an example, which I've oriented by hand, showing the Senda Pirenáica in Spain: 这是一个例子,我手工定向,展示了西班牙的SendaPirenáica:

屏幕截图

The area of interest, the viewport, and the pitch are given. 给出了感兴趣的区域,视口和音高。 I need to find the correct center, bearing, and zoom. 我需要找到正确的中心,方位和缩放。

The map.fitBounds method doesn't help me here because it assumes pitch=0 and bearing=0. map.fitBounds方法对我没有帮助,因为它假设pitch = 0且bearing = 0。

I've done some poking around and this seems to be a variation of the smallest surrounding rectangle problem, but I'm stuck on a couple of additional complications: 我做了一些戳,这似乎是最小的周围矩形问题的变化,但我仍然坚持一些额外的复杂性:

  1. How do I account for the distorting effect of pitch? 我如何解释音高的扭曲效果?
  2. How do I optimize for the aspect ratio of the viewport? 如何优化视口的宽高比? Note that taking the viewport narrower or wider would change the bearing of the best solution: 请注意,将视口缩小或更宽会改变最佳解决方案的方位:

草图

FWIW I'm also using turf-js, which helps me get the convex hull for the line. FWIW我也使用turf-js,这可以帮助我获得线的凸包。

This solution results in the path displayed at the correct bearing with a magenta trapezoid outline showing the target "tightest trapezoid" to show the results of the calculations. 该解决方案导致在正确方位上显示的路径具有品红色梯形轮廓,显示目标“最紧密的梯形”以显示计算结果。 The extra line coming from the top corner shows where the map.center() value is located. 来自顶角的额外行显示了map.center()值的位置。

The approach is as follows: 方法如下:

  1. render the path to the map using the "fitbounds" technique to get an approximate zoom level for the "north up and pitch=0" situation 使用“fitbounds”技术渲染地图的路径,以获得“北向上和俯仰= 0”情况的近似缩放级别
  2. rotate the pitch to the desired angle 将音高旋转到所需的角度
  3. grab the trapezoid from the canvas 抓住画布上的梯形

This result would look like this: 这个结果看起来像这样:

初始视图梯形

After this, we want to rotate that trapezoid around the path and find the tightest fit of the trapezoid to the points. 在此之后,我们想要在路径周围旋转该梯形,并找到梯形最紧密的点。 In order to test for the tightest fit it is easier to rotate the path rather than the trapezoid so I have taken that approach here. 为了测试最紧密的配合,更容易旋转路径而不是梯形,所以我在这里采取了这种方法。 I haven't implemented a "convex hull" on the path to minimize the number of points to rotate but that is something that can be added as an optimization step. 我没有在路径上实现“凸包”以最小化旋转的点数,但这可以作为优化步骤添加。
To get the tightest fit, the first step is to move the map.center() so that the path is at the "back" of the view. 为了获得最紧密的匹配,第一步是移动map.center(),使路径位于视图的“后面”。 This is where the most space is in the frustum so it will be easy to manipulate it there: 这是最大的空间在平截头体中的位置,因此在那里操作它很容易:

黄色显示调整后的视图位置,将路径放在视图的后面

Next, we measure the distance between the angled trapezoid walls and each point in the path, saving the closest points on both the left and right sides. 接下来,我们测量成角度的梯形墙与路径中的每个点之间的距离,从而节省左侧和右侧的最近点。 We then center the path in the view by translating the view horizontally based on these distances, and then scale the view to eliminate that space on both sides as shown by the green trapezoid below: 然后,我们通过基于这些距离水平平移视图来居中视图中的路径,然后缩放视图以消除两侧的空间,如下面的绿色梯形所示:

绿色梯形显示最小的拟合

The scale used to get this "tightest fit" gives us our ranking for whether this is the best view of the path. 用于获得“最紧密贴合”的尺度给出了我们对这是否是路径的最佳视图的排名。 However, this view may not be the best visually since we pushed the path to the back of the view to determine the ranking. 但是,这个视图可能不是最好的视觉效果,因为我们将路径推到视图的后面以确定排名。 Instead, we now adjust the view to place the path in the vertical center of the view, and scale the view triangle larger accordingly. 相反,我们现在调整视图以将路径放置在视图的垂直中心,并相应地缩放视图三角形。 This gives us the magenta colored "final" view desired: 这为我们提供了所需的洋红色“最终”视图:

洋红色的最终视图。

Finally, this process is done for every degree and the minimum scale value determines the winning bearing, and we take the associated scale and center position from there. 最后,这个过程是针对每个度数完成的,最小比例值决定了获胜方位,我们从那里获取相关的比例和中心位置。

 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> 

Smallest surrounding rectangle would be specific to pitch=0 (looking directly down). 最小的周围矩形将特定于pitch = 0(直接向下看)。

One option is to continue with smallest surrounding rectangle approach and calculate the transformation of the target area - just like a 3d engine does. 一种选择是继续使用最小的周围矩形方法并计算目标区域的变换 - 就像3d引擎一样。 If this is what you do maybe skim through unity docs to better understand the mechanics of viewing frustum 如果这是您所做的,可以浏览统一文档以更好地理解查看视锥体的机制

I feel this wouldn't be appropriate for your problem though as you'd have to re-calculate a 2d rendering of the target area from different angles, a relatively expensive brute force. 我觉得这不适合你的问题,因为你必须从不同角度重新计算目标区域的2D渲染,这是一种相对昂贵的蛮力。

Another way to normalize the calculation would be to render a viewport projection into target area plane. 规范化计算的另一种方法是将视口投影渲染到目标区域平面中。 See for yourself: 你自己看:

粗略投影

Then all you have to do is "just" figure out the largest size your original convex hull can fit into a trapezoid of that shape (specifically a convex isosceles trapezoid since we don't manipulate camera roll). 然后你所要做的就是“只是”找出原始凸壳可以装入该形状的梯形的最大尺寸(特别是凸起的等腰梯形,因为我们不操纵相机滚动)。

This is where I get a little out of depth and don't know where to point you for a calculation. 这是我有点深入的地方,不知道在哪里指出你的计算。 I figure it's at least cheaper to iterate over possible solutions in this 2D space though. 我认为在这个2D空间中迭代可能的解决方案至少会更便宜。

PS: One more thing to keep in mind is the viewport projection shape will be different depending on FOV (field of view). PS:要记住的另一件事是视口投影形状将根据FOV(视野)而不同。

This changes when you resize the browser viewport, but the property doesn't seem to be exposed in mapbox-gl-js. 当您调整浏览器视口大小时,这会更改,但该属性似乎没有在mapbox-gl-js中公开。

Edit: 编辑:

After some thought I feel the best mathematical solution can feel a little "dry" in reality. 经过一番思考后,我觉得最好的数学解决方案在现实中会感觉有点“干”。 Not being across the use case and, possibly, making some wrong assumptions, I'd ask these questions: 不是跨越用例,并且可能做出一些错误的假设,我会问这些问题:

  • For a route that's roughly a straight line, would it always be panned in so the ends are at bottom left and top right corners? 对于大致是直线的路线,它是否总是被平移,因此末端位于左下角和右上角? That would be close to "optimal" but could get... boring. 这将接近“最佳”,但可能会变得......无聊。
  • Would you want to keep more of the path closer to the viewport? 你想让更多的路径更靠近视口吗? You can lose route detail if a large portion of it is far away from the viewport. 如果大部分距离视口很远,则可能会丢失路径详细信息。
  • Would you pick points of interest to focus on? 你会选择关注点吗? Those could be closer to the viewport. 那些可能更接近视口。

Perhaps it would be handy to classify different types of routes by shape of hull and create panning presets? 也许通过船体形状对不同类型的路线进行分类并创建平移预设会很方便吗?

Hopefully this can point you in the right direction with some tweaking. 希望这可以通过一些调整指向正确的方向。

First I set up the two points we want to show 首先,我设置了我们要展示的两点

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

Then I found the middle of those two points. 然后我发现了这两点的中间部分。 I made my own function for this, but it looks like turf can do this. 我为此制作了自己的功能,但看起来草皮可以做到这一点。

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])]

I used turfs bearing function to have the view from the 2nd point look at the first point 我使用turfs轴承功能从第二点看第一点的视图

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

Then I call the map and run the fitBounds function: 然后我调用地图并运行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})

Here's a codepen: https://codepen.io/thejoshderocher/pen/BRYGXq 这是一个codepen: https ://codepen.io/thejoshderocher/pen/BRYGXq

To adjust the bearing to best use the screen size is to get the size of the window and adjust the bearing to take the most advantage of the available screen space. 要调整轴承以最好地使用屏幕尺寸是为了获得窗口的大小并调整轴承以充分利用可用的屏幕空间。 If it's a mobile screen in portrait, this bearing work perfect. 如果它是一个纵向移动屏幕,这个轴承工作完美。 If you are on a desktop with a wide view you will need to rotate so point A is in one of the top corners. 如果您在具有宽视图的桌面上,则需要旋转,以使A点位于其中一个顶角。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM