카메라 ― 추상화의 가장 어려운 부분
MapConductor에서 가장 어렵고, 가장 가치 있는 것이 카메라의 통일입니다. position・zoom・bearing・tilt・visibleRegion으로 이루어진 하나의 카메라 모델을, 10종류 이상의 지도 엔진에서 같은 의미로 맞춰 놓았습니다. 같은 MapCameraPosition을 넘기면, 어느 프로바이더에서도 같은 풍경이 나옵니다.
01 · 왜 통일에 의미가 있는가
지도 엔진은 저마다 다른 카메라 모델을 가지고 있습니다. 줌으로 말하는 것, 카메라 고도로 말하는 것, 지상의 주시점을 중심으로 도는 것, 시점 자체를 움직이는 것. 네이티브 SDK를 그대로 쓰면, 프로바이더를 교체한 순간에 「같은 수치인데 다른 풍경」이 되고, 화면의 로직까지 다시 쓰게 됩니다.
MapConductor는 이 차이를 어댑터 층에 가두고, 앱에서는 하나의 카메라 모델만을 보여 줍니다. 그 결과, 카메라 위치는 프로바이더를 넘어 들고 다닐 수 있는 값이 됩니다. 샘플 앱의 Camera Sync 페이지에서는, 하나의 카메라 위치를 모든 프로바이더에 흘려 넣어, 같은 지점・같은 축척・같은 기울기로 늘어서는 것을 실제로 확인할 수 있습니다.
줌인가, 고도인가
2D 타일 계열은 줌 레벨, 3D 계열은 카메라 고도나 시거리로 축척을 나타냅니다. 단위도 기준도 다릅니다.
주시점인가, 시점인가
지상의 한 점을 중심으로 도는(오빗) 모델과, 카메라 자신의 위치와 방향으로 말하는 모델이 섞여 있습니다.
기울기의 허용 범위
최대 경사각도, 위쪽으로 기울일 수 있는지도, 엔진마다 다릅니다. 음의 기울기를 가지는 것은 하나도 없습니다.
val camera = MapCameraPosition(
position = geoPoint,
zoom = 18.0,
tilt = 70.0,
)
moveCameraTo(camera)02 · 통일 카메라 모델
카메라는 MapCameraPosition 하나로 나타냅니다. 프로바이더마다의 타입은 없고, 이 값을 만들어 moveCameraTo에 넘기기만 하면 됩니다.
copy()로 일부만 교체할 수 있고, 비교는 줌 0.01・기울기 0.01의 허용 오차를 두고 이루어집니다. 프로바이더에서 돌아오는 값의 미세한 흔들림으로 「움직였다」고 오판정하지 않기 위해서입니다.
03 · 줌 레벨=높이의 통일
줌은, 내부에서는 「카메라의 높이」와 같은 것으로 다뤄집니다. 2D 타일 계열의 줌, 3D 계열의 고도・시거리를 상호 변환할 수 있는 컨버터가 core에 있고, 어느 프로바이더의 값이든 일단 공통의 줌(Google Maps 기준)으로 고친 뒤에 공개됩니다.
이 기준은 이론만으로 정한 것이 아닙니다. 샘플 앱의 캘리브레이션 도구로, 같은 줌을 지정했을 때 실제로 보이는 범위를 각 엔진에서 실측하고, 면적이 일치하도록 기준 고도를 교정하고 있습니다. 그래서 zoom 14는, 어느 프로바이더에서도 같은 넓이입니다.
같은 줌이라도 위도에 따라 필요한 높이가 다릅니다
웹 메르카토르는 극지방으로 갈수록 지도가 확대되므로, 같은 줌 레벨이라도 적도 부근과 고위도에서는 필요한 카메라 고도가 몇 배 차이 납니다. 줌 ⇄ 고도 변환에는 이 보정이 들어 있습니다.
2배마다 1단이라는 관계와, 기준이 되는 zoom 0의 고도만이 상수입니다. 위도가 높을수록 같은 줌에서의 지상 거리가 줄어드는 점도 변환에 들어 있기 때문에, 도쿄와 호놀룰루에서 축척이 어긋나는 일은 없습니다.
04 · tilt < 0의 의사 지원
tilt는 −60〜60도를 취합니다. 양의 값은 익숙한 비스듬한 내려다보기이지만, 음의 값은 수평선보다 위를 보는 앙각 뷰입니다. 지도 엔진에는, 이것을 직접 표현할 수 있는 것이 하나도 없습니다. 어느 SDK도 pitch는 0 이상만 받아들입니다.
그래서 MapConductor는, 카메라의 눈 위치와 방향을 유지한 채로, 엔진에는 양의 기울기로 주는 형태로 바꿔 놓습니다. 앱에서 보면 음의 tilt는 그대로 쓸 수 있고, 취득한 카메라 위치도 음의 tilt로 돌아옵니다.
바로 위에서 내려다보는 평면도. 지상의 사각형이 그대로 화면의 사각형이 됩니다.
앞으로 엎드린 비스듬한 시점. 지상에서는 앞이 좁고 뒤가 넓은 사다리꼴이 됩니다.
수평선보다 위를 향하는 앙각 뷰. 하늘 쪽을 포함하는 시점으로, 네이티브에는 존재하지 않습니다.
예외는 React의 Google Maps 2D(JavaScript API의 래스터 지도)뿐입니다. 기울기가 브라우저와 타일 쪽 조건에 좌우되어, 의사 표현이 전제로 하는 고도와 방향을 제어할 수 없기 때문입니다. Google Maps 3D(Map3DElement)에서는 음수 tilt를 쓸 수 있습니다.
// −60〜60 슬라이더를 그대로 넘기기만 하면 된다. 음수 값이어도 같은 호출 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 ― 실제로 보이고 있는 범위
카메라를 기울이면, 화면의 사각형은 지상에서는 사다리꼴이 됩니다. 남서와 북동만의 사각형(bounds)으로는, 앞과 뒤의 넓이의 차이를 나타낼 수 없습니다. VisibleRegion은 사각형에 더해 화면 네 모서리의 지상 좌표를 가지고, 기울어진 카메라에서도 「정말로 비치고 있는 범위」를 다룰 수 있게 합니다.
near는 화면의 아래 변, 즉 카메라에 가까운 쪽, far는 위 변으로 먼 쪽입니다. left / right는 화면 기준이므로, bearing으로 지도를 돌려도 의미는 달라지지 않습니다. 이 4점이 갖춰져 있는 덕분에, 기울어진 지도에서도 데이터 취득 범위나 출처 표기의 판정을 올바르게 할 수 있습니다.
mapViewState.cameraPosition.visibleRegion?.let { region ->
val sw = region.bounds.southWest
val ne = region.bounds.northEast
// 기울어진 화면의 네 모서리. 사다리꼴로 다룰 수 있다
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}>
{/* cameraPosition.visibleRegion의 내용을 나열해 표시한다 */}
</MapViewContainer>
const visibleRegion = cameraPosition?.visibleRegion ?? null;
visibleRegion?.nearLeft?.toUrlValue(5);
visibleRegion?.farRight?.toUrlValue(5);