javascript - moveend - mapboxgl marker
mapbox-gl-js: ajusta el área visible y el rumbo a una línea dada, para un tono dado (3)
Estoy tratando de optimizar una vista de Mapbox para rutas de senderismo de larga distancia, como Appalachian Trail o Pacific Crest Trail. Aquí hay un ejemplo, que he orientado a mano, mostrando la Senda Pirenáica en España:
Se dan el área de interés, la ventana gráfica y el campo. Necesito encontrar el centro, el rumbo y el zoom correctos.
El método map.fitBounds
no me ayuda aquí porque asume pitch = 0 y bearing = 0.
He hecho algunos intentos de hurgar y esto parece ser una variación del problema del rectángulo circundante más pequeño , pero estoy atascado en un par de complicaciones adicionales:
- ¿Cómo tengo en cuenta el efecto distorsionador del tono?
- ¿Cómo puedo optimizar la relación de aspecto de la ventana gráfica? Tenga en cuenta que tomar la ventana gráfica más estrecho o más ancho cambiaría la orientación de la mejor solución:
FWIW También estoy usando turf-js, que me ayuda a obtener el casco convexo para la línea.
El rectángulo circundante más pequeño sería específico para el tono = 0 (mirando directamente hacia abajo).
Una opción es continuar con el enfoque de rectángulo circundante más pequeño y calcular la transformación del área objetivo, tal como lo hace un motor 3D. Si esto es lo que hace, tal vez hojee los documentos de Unity para comprender mejor la mecánica de visualización del frustum.
Creo que esto no sería apropiado para tu problema, ya que tendrías que volver a calcular una representación 2D del área objetivo desde diferentes ángulos, una fuerza bruta relativamente costosa.
Otra forma de normalizar el cálculo sería representar una proyección de ventana gráfica en el plano del área objetivo. Ver por ti mismo:
Entonces todo lo que tiene que hacer es "simplemente" averiguar el tamaño más grande que su casco convexo original puede encajar en un trapezoide de esa forma (específicamente un trapezoide isósceles convexo ya que no manipulamos el giro de la cámara).
Aquí es donde obtengo un poco de profundidad y no sé dónde señalarlo para un cálculo. Sin embargo, me imagino que al menos es más barato iterar sobre posibles soluciones en este espacio 2D.
PD: una cosa más a tener en cuenta es que la forma de proyección de la ventana gráfica será diferente según el FOV (campo de visión).
Esto cambia cuando cambia el tamaño de la ventana de visualización del navegador , pero la propiedad no parece estar expuesta en mapbox-gl-js.
Editar:
Después de pensar un poco, siento que la mejor solución matemática puede sentirse un poco "seca" en realidad. Al no estar en el caso de uso y, posiblemente, hacer algunas suposiciones erróneas, me gustaría hacer estas preguntas:
- Para una ruta que es más o menos una línea recta, ¿siempre se integrará de manera que los extremos queden en las esquinas inferior izquierda y superior derecha? Eso estaría cerca de "óptimo" pero podría ser ... aburrido.
- ¿Desearía mantener un poco más cerca de la ventana gráfica? Puede perder detalles de la ruta si una gran parte de ella está lejos de la ventana gráfica.
- ¿Elegiría puntos de interés para enfocarse? Aquellos podrían estar más cerca de la vista.
¿Quizás sería útil clasificar diferentes tipos de rutas por la forma del casco y crear ajustes preestablecidos de panoramización?
Esperemos que esto pueda apuntarle en la dirección correcta con algunos ajustes.
Primero configuro los dos puntos que queremos mostrar.
let pointA = [-70, 43]
let pointB = [-83, 32]
Luego encontré la mitad de esos dos puntos. Hice mi propia función para esto, pero parece que el césped puede hacer esto.
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])]
Usé la función de cojinete de césped para que la vista desde el segundo punto mire el primer punto
let p1 = turf.point(pointA)
let p2 = turf.point(pointB)
let points = turf.featureCollection([p1, p2])
let bearing = turf.bearing(p2, p1)
Luego llamo al mapa y ejecuto la función 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})
Aquí hay un codepen: https://codepen.io/thejoshderocher/pen/BRYGXq
Para ajustar el rumbo al mejor uso del tamaño de la pantalla es obtener el tamaño de la ventana y ajustar el rodamiento para aprovechar al máximo el espacio disponible en la pantalla. Si se trata de una pantalla móvil en retrato, este rodamiento funciona perfectamente. Si está en un escritorio con una vista amplia, tendrá que girar para que el punto A se encuentre en una de las esquinas superiores.
Esta solución da como resultado la ruta que se muestra en el rumbo correcto con un contorno de trapecio magenta que muestra el objetivo "trapecio más ajustado" para mostrar los resultados de los cálculos. La línea adicional que viene de la esquina superior muestra dónde se encuentra el valor de map.center ().
El enfoque es el siguiente:
- renderice el camino hacia el mapa usando la técnica "fitbounds" para obtener un nivel de zoom aproximado para la situación "norte arriba y tono = 0"
- gire el tono al ángulo deseado
- agarrar el trapecio de la lona
Este resultado se vería así:
Después de esto, queremos rotar ese trapecio alrededor del camino y encontrar el ajuste más apretado del trapecio a los puntos. Para probar el ajuste más ajustado, es más fácil rotar la trayectoria en lugar del trapecio, por lo que he tomado ese enfoque aquí. No he implementado un "casco convexo" en la ruta para minimizar el número de puntos a rotar, pero eso es algo que se puede agregar como un paso de optimización.
Para obtener el ajuste más preciso, el primer paso es mover el map.center () de modo que la ruta se encuentre en la "parte posterior" de la vista. Aquí es donde la mayor cantidad de espacio está en el tronco por lo que será fácil manipularlo allí:
A continuación, medimos la distancia entre las paredes trapezoidales en ángulo y cada punto en el camino, guardando los puntos más cercanos en los lados izquierdo y derecho. Luego centramos el camino en la vista al traducir la vista horizontalmente en función de estas distancias, y luego escalamos la vista para eliminar ese espacio en ambos lados como lo muestra el trapecio verde a continuación:
La escala utilizada para obtener este "ajuste más estrecho" nos da nuestra clasificación para determinar si esta es la mejor vista del camino. Sin embargo, esta vista puede no ser la mejor visualmente ya que empujamos el camino hacia la parte posterior de la vista para determinar la clasificación. En su lugar, ahora ajustamos la vista para colocar la ruta en el centro vertical de la vista y escalamos el triángulo de la vista más grande en consecuencia. Esto nos da la vista "final" de color magenta deseada:
Finalmente, este proceso se realiza para cada grado y el valor de escala mínimo determina el rumbo ganador, y tomamos la escala asociada y la posición central desde allí.
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>