23

アパラチアン トレイルやパシフィック クレスト トレイルなどの長距離ハイキング トレイル用に Mapbox ビューを最適化しようとしています。以下は、スペインのセンダ ピレナイカを示す例です。

画面キャプチャ

関心のある領域、ビューポート、およびピッチが与えられます。正しい中心、方位、ズームを見つける必要があります。

このmap.fitBounds方法は、ピッチ = 0 とベアリング = 0 を想定しているため、ここでは役に立ちません。

私はいくつか突っついてきまし.

  1. ピッチの歪み効果をどのように説明しますか?
  2. ビューポートの縦横比を最適化するにはどうすればよいですか? ビューポートを狭くしたり広くしたりすると、最適解の方位が変わることに注意してください。

スケッチ

FWIW 私は turf-js も使用しています。これは、ラインの凸包を取得するのに役立ちます。

4

3 に答える 3

15

このソリューションは、計算の結果を示すために、ターゲットの「最もタイトな台形」を示すマゼンタの台形のアウトラインで正しい方位に表示されるパスになります。上隅から出ている余分な線は、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>

于 2017-05-10T17:34:04.453 に答える