Cámara: la parte más difícil de la abstracción
En MapConductor, la unificación de la cámara es lo más difícil y valioso. Un modelo de cámara compuesto por position, zoom, bearing, tilt y visibleRegion se ha estandarizado con el mismo significado en más de 10 motores de mapas. Si se pasa la misma MapCameraPosition, se obtiene la misma vista con cualquier proveedor.
01 · Por qué tiene sentido unificar
Cada motor de mapas tiene su propio modelo de cámara. Algunos hablan de zoom, otros de altitud de la cámara, unos giran alrededor de un punto de interés en el suelo, otros mueven el punto de vista en sí. Si se usa el SDK nativo tal cual, al cambiar de proveedor se obtiene instantáneamente "mismo número pero vista diferente", lo que obliga a reescribir la lógica de la pantalla.
MapConductor encapsula esta diferencia en una capa de adaptador y expone a la aplicación un único modelo de cámara. Como resultado, la posición de la cámara se convierte en un valor que se puede transportar entre proveedores. En la página Camera Sync de la aplicación de ejemplo, se puede comprobar cómo una única posición de cámara se envía a todos los proveedores y se alinean en el mismo lugar, la misma escala y la misma inclinación.
¿Zoom o altitud?
Los sistemas de mosaicos 2D expresan la escala mediante el nivel de zoom, mientras que los sistemas 3D lo hacen mediante la altitud de la cámara o la distancia de visión. Las unidades y las referencias también difieren.
¿Punto de enfoque o punto de vista?
Conviven un modelo que gira alrededor de un punto en la superficie (órbita) y un modelo que se basa en la posición y orientación de la propia cámara.
Rango de inclinación permitido
El ángulo máximo de inclinación y si se puede inclinar hacia arriba varían según el motor. Ninguno tiene una inclinación negativa.
val camera = MapCameraPosition(
position = geoPoint,
zoom = 18.0,
tilt = 70.0,
)
moveCameraTo(camera)02 · Modelo de cámara unificado
La cámara se representa con un único MapCameraPosition. No hay tipos por proveedor; simplemente se crea este valor y se pasa a moveCameraTo.
Con copy() se pueden reemplazar solo partes; la comparación se realiza con una tolerancia de zoom 0,01 y inclinación 0,01. Así se evita que fluctuaciones menores de los valores devueltos por el proveedor se interpreten erróneamente como "movido".
03 · Unificación de nivel de zoom = altura
El zoom se trata internamente igual que la “altura de la cámara”. En `core` hay convertidores que pueden convertir mutuamente el zoom de teselas 2D y la altura/distancia de visión 3D; los valores de todos los proveedores se convierten primero a un zoom común (estándar de Google Maps) antes de publicarse.
Este estándar no se decidió solo por teoría. Con la herramienta de calibración de la app de ejemplo, se midió el área realmente visible para cada motor al especificar el mismo zoom, y se calibró la altura estándar para que las áreas coincidan. Por eso, el zoom 14 tiene el mismo ancho en todos los proveedores.
El mismo zoom necesita una altura distinta según la latitud
Web Mercator amplía el mapa hacia los polos, así que la altitud de cámara para un mismo nivel de zoom cambia varias veces entre el ecuador y latitudes altas. La conversión zoom ⇄ altitud ya lo tiene en cuenta.
Lo único constante es la relación “un paso por cada duplicación” y la altura del zoom 0 como referencia. También se incluye en la conversión el hecho de que la distancia en el suelo con el mismo zoom se reduce a latitudes más altas, por lo que la escala no se desajusta entre Tokio y Honolulu.
04 · Soporte pseudo para tilt < 0
tilt acepta valores de −60 a 60 grados. Los valores positivos son la vista en ángulo habitual, mientras que los valores negativos son una vista de elevación mirando por encima del horizonte. Ningún motor de mapas puede expresar esto directamente; todos los SDK solo aceptan pitch ≥ 0.
Por eso, MapConductor reemplaza esto manteniendo la posición y la orientación de la cámara, pero pasándole al motor un valor de inclinación positivo. Desde la app se puede usar tilt negativo tal cual, y la posición de cámara obtenida también se devuelve como tilt negativo.
Un plano visto desde directamente arriba. Un rectángulo en la superficie terrestre se convierte directamente en un rectángulo en la pantalla.
Una perspectiva en diagonal inclinada hacia adelante. En la superficie terrestre se convierte en un trapecio más estrecho en la parte cercana y más ancho en la parte lejana.
Una vista elevada por encima del horizonte. Un punto de vista que incluye el cielo, que no existe de forma nativa.
La única excepción es Google Maps 2D en React: el mapa ráster de la API de JavaScript. Su inclinación depende del navegador y de las teselas, así que no se pueden controlar la altitud ni la orientación en que se apoya la emulación. Google Maps 3D (Map3DElement) sí admite inclinación negativa.
// Pasa el deslizador de −60 a 60 tal cual; un valor negativo es la misma llamada currentPosition = currentPosition.copy(tilt = tilt) mapViewState.value?.moveCameraTo(currentPosition)
func setTilt(_ angle: Double, state: any MapViewStateProtocol) {
currentPosition = currentPosition.copy(tilt: angle)
state.moveCameraTo(cameraPosition: currentPosition)
}<SliderControl
label="Tilt" value={tilt} min={-60} max={60} step={1}
onChange={value => {
setTilt(value);
const next = cameraPositionRef.current.copy({ tilt: value });
cameraPositionRef.current = next;
mapViewState.moveCameraTo(next, 400);
}}
/>05 · VisibleRegion — el área realmente visible
Al inclinar la cámara, el rectángulo de la pantalla se convierte en un trapecio en el suelo. Un rectángulo que solo incluye sudoeste y noreste (bounds) no puede expresar la diferencia de ancho entre el primer plano y el fondo. VisibleRegion tiene, además del rectángulo, las coordenadas en el suelo de las cuatro esquinas de la pantalla, lo que permite manejar “el área realmente mostrada” incluso con cámara inclinada.
near es el borde inferior de la pantalla, es decir, el lado más cercano a la cámara; far es el borde superior, el lado más alejado. left / right están basados en la pantalla, por lo que su significado no cambia aunque se gire el mapa con bearing. Gracias a estos cuatro puntos, incluso con mapa inclinado se pueden determinar correctamente el área de obtención de datos y la atribución de fuentes.
mapViewState.cameraPosition.visibleRegion?.let { region ->
val sw = region.bounds.southWest
val ne = region.bounds.northEast
// Las cuatro esquinas de una pantalla inclinada, tratables como un trapecio
val corners = listOf(region.nearLeft, region.nearRight, region.farLeft, region.farRight)
}if let region = cameraPosition.visibleRegion {
let bounds = region.bounds
let corners = [region.nearLeft, region.nearRight, region.farLeft, region.farRight]
}<MapViewContainer initialCamera={INIT_CAMERA} onCameraMove={setCameraPosition}>
{/* Muestra en lista el contenido de cameraPosition.visibleRegion */}
</MapViewContainer>
const visibleRegion = cameraPosition?.visibleRegion ?? null;
visibleRegion?.nearLeft?.toUrlValue(5);
visibleRegion?.farRight?.toUrlValue(5);