簡體   English   中英

mapbox-gl-js:對於給定的音高,調整可見區域和軸承到給定的線

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

我正在嘗試優化長途遠足徑的Mapbox視圖,例如Appalachian Trail或Pacific Crest Trail。 這是一個例子,我手工定向,展示了西班牙的SendaPirenáica:

屏幕截圖

給出了感興趣的區域,視口和音高。 我需要找到正確的中心,方位和縮放。

map.fitBounds方法對我沒有幫助,因為它假設pitch = 0且bearing = 0。

我做了一些戳,這似乎是最小的周圍矩形問題的變化,但我仍然堅持一些額外的復雜性:

  1. 我如何解釋音高的扭曲效果?
  2. 如何優化視口的寬高比? 請注意,將視口縮小或更寬會改變最佳解決方案的方位:

草圖

FWIW我也使用turf-js,這可以幫助我獲得線的凸包。

該解決方案導致在正確方位上顯示的路徑具有品紅色梯形輪廓,顯示目標“最緊密的梯形”以顯示計算結果。 來自頂角的額外行顯示了map.center()值的位置。

方法如下:

  1. 使用“fitbounds”技術渲染地圖的路徑,以獲得“北向上和俯仰= 0”情況的近似縮放級別
  2. 將音高旋轉到所需的角度
  3. 抓住畫布上的梯形

這個結果看起來像這樣:

初始視圖梯形

在此之后,我們想要在路徑周圍旋轉該梯形,並找到梯形最緊密的點。 為了測試最緊密的配合,更容易旋轉路徑而不是梯形,所以我在這里采取了這種方法。 我沒有在路徑上實現“凸包”以最小化旋轉的點數,但這可以作為優化步驟添加。
為了獲得最緊密的匹配,第一步是移動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> 

最小的周圍矩形將特定於pitch = 0(直接向下看)。

一種選擇是繼續使用最小的周圍矩形方法並計算目標區域的變換 - 就像3d引擎一樣。 如果這是您所做的,可以瀏覽統一文檔以更好地理解查看視錐體的機制

我覺得這不適合你的問題,因為你必須從不同角度重新計算目標區域的2D渲染,這是一種相對昂貴的蠻力。

規范化計算的另一種方法是將視口投影渲染到目標區域平面中。 你自己看:

粗略投影

然后你所要做的就是“只是”找出原始凸殼可以裝入該形狀的梯形的最大尺寸(特別是凸起的等腰梯形,因為我們不操縱相機滾動)。

這是我有點深入的地方,不知道在哪里指出你的計算。 我認為在這個2D空間中迭代可能的解決方案至少會更便宜。

PS:要記住的另一件事是視口投影形狀將根據FOV(視野)而不同。

當您調整瀏覽器視口大小時,這會更改,但該屬性似乎沒有在mapbox-gl-js中公開。

編輯:

經過一番思考后,我覺得最好的數學解決方案在現實中會感覺有點“干”。 不是跨越用例,並且可能做出一些錯誤的假設,我會問這些問題:

  • 對於大致是直線的路線,它是否總是被平移,因此末端位於左下角和右上角? 這將接近“最佳”,但可能會變得......無聊。
  • 你想讓更多的路徑更靠近視口嗎? 如果大部分距離視口很遠,則可能會丟失路徑詳細信息。
  • 你會選擇關注點嗎? 那些可能更接近視口。

也許通過船體形狀對不同類型的路線進行分類並創建平移預設會很方便嗎?

希望這可以通過一些調整指向正確的方向。

首先,我設置了我們要展示的兩點

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

然后我發現了這兩點的中間部分。 我為此制作了自己的功能,但看起來草皮可以做到這一點。

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

我使用turfs軸承功能從第二點看第一點的視圖

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

這是一個codepen: https ://codepen.io/thejoshderocher/pen/BRYGXq

要調整軸承以最好地使用屏幕尺寸是為了獲得窗口的大小並調整軸承以充分利用可用的屏幕空間。 如果它是一個縱向移動屏幕,這個軸承工作完美。 如果您在具有寬視圖的桌面上,則需要旋轉,以使A點位於其中一個頂角。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM