문서 / 코어 / 투영과 줌

투영과 줌

둥근 지구를 평평한 화면에 떨어뜨리는 것이 투영이고, 어디까지 다가가 있는지를 나타내는 것이 줌입니다. 프로바이더마다 어느 정의도 제각각이지만, MapConductor는 같은 상수와 같은 식을 3개 플랫폼에서 공유하고, 공개되는 값을 하나의 척도로 맞춥니다. 카메라의 사용법은 카메라 페이지를, 여기서는 그 아래에 있는 수치를 다룹니다.

ELLIPSOID
WGS84
PROJECTIONS
WebMercator · WGS84
ZOOM RANGE
0 – 22
플랫폼

01 · 지구의 상수

투영・지리 계산・타일 계산은 모두 Earth의 상수를 참조합니다.

상수
설명
RADIUS_METERS
6 378 137.0
WGS84의 장반경(적도 반경).
CIRCUMFERENCE_METERS
2πa
적도의 전체 둘레. 타일의 분해능 계산에 씁니다.
FLATTENING
1 / 298.257223563
편평률. 타원체 모델의 측지선 계산에 씁니다.
SEMI_MINOR_AXIS_METERS
a(1 − f)
단반경(극반경).
ECCENTRICITY_SQUARED
f(2 − f)
제1이심률의 제곱.
WEB_MERCATOR_MAX_EXTENT_METERS
πa ≈ 20 037 508.34
웹 메르카토르의 좌표가 취할 수 있는 최댓값. 투영 후의 x・y는 이 범위에 들어갑니다.

02 · 투영

투영의 타입은 project와 unproject 2개만 가집니다. 좌표를 평면의 좌표(Offset)로 떨어뜨리고, 다시 되돌리는 대칭적인 조작입니다. 구현은 2가지 마련되어 있습니다.

WebMercator

미터 단위의 웹 메르카토르

EPSG:3857과 같은, ±20,037,508.34 m의 평면으로 떨어뜨립니다. 타일 경계의 계산이나, 화면상의 거리를 미터로 환산하는 처리에서 씁니다.

WGS84

256 픽셀의 타일 좌표

줌 0의 타일 한 장(256×256)을 단위로 하는 좌표로 떨어뜨립니다. 타일 이미지를 생성하는 렌더러가 쓰는 좌표계입니다.

com.mapconductor.core.projection
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까지의 타일 좌표

화면상의 픽셀과 좌표의 상호 변환이 필요할 때는, 투영이 아니라 MapViewHolder의 toScreenOffset / fromScreenOffset을 쓰세요. 실제로 표시되고 있는 카메라의 기울기나 회전이 반영됩니다.

03 · 줌과 고도의 변환

2D 타일 계열의 프로바이더는 줌 레벨로, 3D 계열은 카메라 고도나 시거리로 축척을 나타냅니다. MapConductor는 이 2가지를 상호 변환해, 앱에는 항상 같은 줌 척도로 보여 줍니다. 변환은 프로바이더마다 1클래스, 공통의 추상 클래스를 상속하는 형태로 구현되어 있습니다.

변환에는 위도와 기울기가 들어갑니다. 웹 메르카토르는 고위도일수록 지도를 확대하기 때문에, 같은 줌이라도 적도와 고위도에서는 필요한 카메라 고도가 몇 배 다르기 때문입니다.

그림 · 같은 줌 값에서 필요한 카메라 고도(적도를 1로 한 비)
0° · 적도
×1.00
30° · 카이로
×0.87
35° · 도쿄
×0.82
같은 zoom 14라도, 적도보다 2할 정도 낮은 위치에서 보고 있는 셈이 됩니다.
60° · 오슬로
×0.50
70° · 트롬쇠
×0.34
적도와의 차는 대략 3배. 위도를 무시하고 고도만 맞추면, 보이는 범위가 이만큼 어긋납니다.
AbstractZoomAltitudeConverter · Kotlin
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
}

컨버터는 각 프로바이더의 패키지가 가집니다(ZoomAltitudeConverter). 앱이 직접 호출할 필요는 없지만, 독자적인 3D 표현을 조립할 때는 쓸 수 있습니다.

영상으로 보여 주는 구현 · Android + MapLibreKotlin · Jetpack Compose
// 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) },
    )
}
샘플 영상 · 같은 줌 값을 프로바이더 간에 비교하기
영상 미촬영같은 zoom을 준 채로 프로바이더를 전환해, 실제의 축척이 얼마나 어긋나는지를 보여 줍니다.

04 · 경계값

변환에 쓰이는 상수입니다. 극 부근이나 바로 위에서 내려다볼 때 식이 발산하지 않도록, cos의 하한도 상수로 정해 두었습니다.

DEFAULT_ZOOM0_ALTITUDE
171,319,879 m
ZOOM_FACTOR
2.0
MIN / MAX_ZOOM_LEVEL
0.0 / 22.0
MIN / MAX_ALTITUDE
100 / 50,000,000 m
MIN_COS_LAT
0.01
MIN_COS_TILT
0.05
WEB_MERCATOR_INITIAL_MPP_256
156,543.033928
TILE SIZE
256 px

zoom 0의 기준 고도는 이론만으로 정한 값이 아닙니다. 같은 줌을 지정했을 때 실제로 보이는 범위를 각 엔진에서 실측하고, 면적이 일치하도록 교정한 결과입니다. 줌 1단에 고도가 절반이 된다(ZOOM_FACTOR = 2)는 관계와, 이 기준값만이 상수입니다.

관련 페이지