투영과 줌
둥근 지구를 평평한 화면에 떨어뜨리는 것이 투영이고, 어디까지 다가가 있는지를 나타내는 것이 줌입니다. 프로바이더마다 어느 정의도 제각각이지만, MapConductor는 같은 상수와 같은 식을 3개 플랫폼에서 공유하고, 공개되는 값을 하나의 척도로 맞춥니다. 카메라의 사용법은 카메라 페이지를, 여기서는 그 아래에 있는 수치를 다룹니다.
01 · 지구의 상수
투영・지리 계산・타일 계산은 모두 Earth의 상수를 참조합니다.
02 · 투영
투영의 타입은 project와 unproject 2개만 가집니다. 좌표를 평면의 좌표(Offset)로 떨어뜨리고, 다시 되돌리는 대칭적인 조작입니다. 구현은 2가지 마련되어 있습니다.
미터 단위의 웹 메르카토르
EPSG:3857과 같은, ±20,037,508.34 m의 평면으로 떨어뜨립니다. 타일 경계의 계산이나, 화면상의 거리를 미터로 환산하는 처리에서 씁니다.
256 픽셀의 타일 좌표
줌 0의 타일 한 장(256×256)을 단위로 하는 좌표로 떨어뜨립니다. 타일 이미지를 생성하는 렌더러가 쓰는 좌표계입니다.
interface ProjectionInterface {
fun project(position: GeoPointInterface): Offset
fun unproject(point: Offset): GeoPointInterface
}
val meters = WebMercator.project(tokyo) // x와 y의 단위는 미터
val tile = WGS84.project(tokyo) // 0부터 256까지의 타일 좌표// iOS에서는 투영이 타일과 공간 인덱스 구현 안에 들어 있습니다. // 앱에서 좌표 ⇄ 화면 변환을 할 때는 MapViewHolder를 쓰세요. let offset = holder.toScreenOffset(position: tokyo) let point = holder.fromScreenOffsetSync(offset: tapLocation)
import { WebMercator, WGS84, Earth } from '@mapconductor/js-sdk-core';
const meters = WebMercator.project(tokyo); // { x, y }의 단위는 미터
const back = WebMercator.unproject(meters);
const tile = WGS84.project(tokyo); // 0부터 256까지의 타일 좌표화면상의 픽셀과 좌표의 상호 변환이 필요할 때는, 투영이 아니라 MapViewHolder의 toScreenOffset / fromScreenOffset을 쓰세요. 실제로 표시되고 있는 카메라의 기울기나 회전이 반영됩니다.
03 · 줌과 고도의 변환
2D 타일 계열의 프로바이더는 줌 레벨로, 3D 계열은 카메라 고도나 시거리로 축척을 나타냅니다. MapConductor는 이 2가지를 상호 변환해, 앱에는 항상 같은 줌 척도로 보여 줍니다. 변환은 프로바이더마다 1클래스, 공통의 추상 클래스를 상속하는 형태로 구현되어 있습니다.
변환에는 위도와 기울기가 들어갑니다. 웹 메르카토르는 고위도일수록 지도를 확대하기 때문에, 같은 줌이라도 적도와 고위도에서는 필요한 카메라 고도가 몇 배 다르기 때문입니다.
abstract class AbstractZoomAltitudeConverter(
protected val zoom0Altitude: Double,
) {
abstract fun zoomLevelToAltitude(zoomLevel: Double, latitude: Double, tilt: Double): Double
abstract fun altitudeToZoomLevel(altitude: Double, latitude: Double, tilt: Double): Double
}public protocol ZoomAltitudeConverterProtocol {
var zoom0Altitude: Double { get }
func zoomLevelToAltitude(zoomLevel: Double, latitude: Double, tilt: Double) -> Double
func altitudeToZoomLevel(altitude: Double, latitude: Double, tilt: Double) -> Double
}export abstract class AbstractZoomAltitudeConverter {
constructor(protected readonly zoom0Altitude: number =
AbstractZoomAltitudeConverter.DEFAULT_ZOOM0_ALTITUDE) {}
abstract zoomLevelToAltitude(params: { zoomLevel: number; latitude: number; tilt: number }): number;
abstract altitudeToZoomLevel(params: { altitude: number; latitude: number; tilt: number }): number;
}컨버터는 각 프로바이더의 패키지가 가집니다(ZoomAltitudeConverter). 앱이 직접 호출할 필요는 없지만, 독자적인 3D 표현을 조립할 때는 쓸 수 있습니다.
// The same camera position handed to both states
val shared = MapCameraPosition(
position = GeoPoint.fromLatLong(35.6812, 139.7671),
zoom = 14.0,
)
val maplibre = rememberMapLibreMapViewState(cameraPosition = shared)
val googlemaps = rememberGoogleMapViewState(cameraPosition = shared)
// Move one and the same value goes to the other
Row {
MapLibreMapView(
state = maplibre,
modifier = Modifier.weight(1f),
onCameraMoveEnd = { googlemaps.moveCameraTo(it) },
)
GoogleMapView(
state = googlemaps,
modifier = Modifier.weight(1f),
onCameraMoveEnd = { maplibre.moveCameraTo(it) },
)
}04 · 경계값
변환에 쓰이는 상수입니다. 극 부근이나 바로 위에서 내려다볼 때 식이 발산하지 않도록, cos의 하한도 상수로 정해 두었습니다.
zoom 0의 기준 고도는 이론만으로 정한 값이 아닙니다. 같은 줌을 지정했을 때 실제로 보이는 범위를 각 엔진에서 실측하고, 면적이 일치하도록 교정한 결과입니다. 줌 1단에 고도가 절반이 된다(ZOOM_FACTOR = 2)는 관계와, 이 기준값만이 상수입니다.