草庐IT

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

coder 2024-05-05 原文

我正在尝试为长距离远足路径优化 Mapbox View ,例如阿巴拉契亚小径或太平洋山脊小径。下面是一个示例,我手动调整了方向,展示了西类牙的 Senda Pirenáica:

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

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

我查了一下,这似乎是 smallest surrounding rectangle 的变体。问题,但我遇到了一些额外的并发症:

  1. 如何解释音高的扭曲效应?
  2. 如何针对视口(viewport)的纵横比进行优化?请注意,使视口(viewport)变窄或变宽会改变最佳解决方案的方位:

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

最佳答案

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

方法如下:

  1. 使用“fitbounds”技术渲染 map 路径以获得“北上和俯仰=0”情况的近似缩放级别
  2. 将音高旋转到所需的 Angular
  3. 从 Canvas 上抓取梯形

这个结果看起来像这样:

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

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

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

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

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>

关于javascript - mapbox-gl-js:针对给定间距将可见区域和方位调整到给定线,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43713462/

有关javascript - mapbox-gl-js:针对给定间距将可见区域和方位调整到给定线的更多相关文章

  1. ruby - 即时确定方法的可见性 - 2

    我正在编写一个方法,它将在一个类中定义一个实例方法;类似于attr_accessor:classFoocustom_method(:foo)end我通过将custom_method函数添加到Module模块并使用define_method定义方法来实现它,效果很好。但我无法弄清楚如何考虑类(class)的可见性属性。例如,在下面的类中classFoocustom_method(:foo)privatecustom_method(:bar)end第一个生成的方法(foo)必须是公共(public)的,第二个(bar)必须是私有(private)的。我怎么做?或者,如何找到调用我的cust

  2. ruby-on-rails - 缺失区域;使用 :region option or export region name to ENV ['AWS_REGION' ] - 2

    我知道还有其他相同的问题,但他们没有解决我的问题。我不断收到错误:Aws::Errors::MissingRegionErrorinBooksController#create,缺少区域;使用:region选项或将区域名称导出到ENV['AWS_REGION']。但是,这是我的配置开发.rb:config.paperclip_defaults={storage::s3,s3_host_name:"s3-us-west-2.amazonaws.com",s3_credentials:{bucket:ENV['AWS_BUCKET'],access_key_id:ENV['AWS_ACCE

  3. ruby-on-rails - 使用 javascript 更改数据方法不会更改 ajax 调用用户的什么方法? - 2

    我遇到了一个非常奇怪的问题,我很难解决。在我看来,我有一个与data-remote="true"和data-method="delete"的链接。当我单击该链接时,我可以看到对我的Rails服务器的DELETE请求。返回的JS代码会更改此链接的属性,其中包括href和data-method。再次单击此链接后,我的服务器收到了对新href的请求,但使用的是旧的data-method,即使我已将其从DELETE到POST(它仍然发送一个DELETE请求)。但是,如果我刷新页面,HTML与"new"HTML相同(随返回的JS发生变化),但它实际上发送了正确的请求类型。这就是这个问题令我困惑的

  4. ruby - 如果满足给定条件,则结束 ruby​​ 程序 - 2

    基本上,我只是试图在满足特定条件时停止程序运行其余行。unlessraw_information.firstputs"Noresultswerereturnedforthatquery"breakend然而,在程序运行之前我得到了这个错误:Invalidbreakcompileerror(SyntaxError)执行此操作的正确方法是什么? 最佳答案 abort("Noresultswerereturnedforthatquery")unlesscondition或unlessconditionabort("Noresultswer

  5. ruby-on-rails - Assets 管道损坏 : Not compiling on the fly css and js files - 2

    我开始了一个新的Rails3.2.5项目,Assets管道不再工作了。CSS和Javascript文件不再编译。这是尝试生成Assets时日志的输出:StartedGET"/assets/application.css?body=1"for127.0.0.1at2012-06-1623:59:11-0700Servedasset/application.css-200OK(0ms)[2012-06-1623:59:11]ERRORNoMethodError:undefinedmethod`each'fornil:NilClass/Users/greg/.rbenv/versions/1

  6. ruby-on-rails - Rails - 理解 application.js 和 application.css - 2

    rails新手。只是想了解\assests目录中的这两个文件。例如,application.js文件有如下行://=requirejquery//=requirejquery_ujs//=require_tree.我理解require_tree。只是将所有JS文件添加到当前目录中。根据上下文,我可以看出requirejquery添加了jQuery库。但是它从哪里得到这些jQuery库呢?我没有在我的Assets文件夹中看到任何jquery.js文件——或者直接在我的整个应用程序中没有看到任何jquery.js文件?同样,我正在按照一些说明安装TwitterBootstrap(http:

  7. ruby - 在 Mechanize 中使用 JavaScript 单击链接 - 2

    我有这个:AccountSummary我想单击该链接,但在使用link_to时出现错误。我试过:bot.click(page.link_with(:href=>/menu_home/))bot.click(page.link_with(:class=>'top_level_active'))bot.click(page.link_with(:href=>/AccountSummary/))我得到的错误是:NoMethodError:nil:NilClass的未定义方法“[]” 最佳答案 那是一个javascript链接。Mechan

  8. ruby - 如何计算自 Ruby 中给定日期以来的周数? - 2

    目标我正在尝试计算自给定日期以来周的距离,而无需跳过任何步骤。我更喜欢用普通的Ruby来做,但ActiveSupport无疑是一个可以接受的选择。我的代码我写了以下内容,这似乎可行,但对我来说似乎还有很长的路要走。require'date'DAYS_IN_WEEK=7.0defweeks_sincedate_stringdate=Date.parsedate_stringdays=Date.today-dateweeks=days/DAYS_IN_WEEKweeks.round2endweeks_since'2015-06-15'#=>32.57ActiveSupport的#weeks

  9. ruby - 根据给定顺序对数字数组进行排序 - 2

    我有两个数组。第一个数组包含排序顺序。第二个数组包含任意数量的元素。我的属性是保证第二个数组中的所有元素(按值)都在第一个数组中,而且我只处理数字。A=[1,3,4,4,4,5,2,1,1,1,3,3]Order=[3,1,2,4,5]当我对A进行排序时,我希望元素按照Order指定的顺序出现:[3,3,3,1,1,1,1,2,4,4,4,5]请注意,重复是公平的游戏。A中的元素不应更改,只能重新排序。我该怎么做? 最佳答案 >>source=[1,3,4,4,4,5,2,1,1,1,3,3]=>[1,3,4,4,4,5,2,1,1

  10. node.js - 如何在 Travis CI 上的一个项目中运行 Node.js 和 Ruby 测试 - 2

    我有一个包含多个组件的存储库,其中大部分是用JavaScript(Node.js)编写的,一个是用Ruby(RubyonRails)编写的。我想要一个.travis.yml文件来触发一个运行每个组件的所有测试的构建。根据thisTravisCIGoogleGroupthread,目前还没有官方支持。我的目录结构是这样的:.├──构建服务器├──核心├──扩展├──网络应用├──流浪文件├──package.json├──.travis.yml└──生成文件我希望能够运行特定版本的Ruby(2.2.2)和Node.js(0.12.2)。我已经有了一个make目标,所以maketest在每

随机推荐