[osmdroid] 안드로이드 지도 개발 - 오버레이

panax·2023년 8월 24일
0

osmdroid

목록 보기
3/4
post-thumbnail

📌오버레이?

osmdroid에서는 지도 위에 마커, 구분선 같은 요소를 추가할 수 있는데, 이것들을 오버레이라고 부른다.
오버레이는 기본으로 제공되는 것 말고도 직접 만들 수 있는데 Overlay 클래스를 상속해서 만들 수 있다.
오버레이는 draw와 onTouchEvent를 구현해서 기능을 만들 수 있다.

class CustomOverlay: Overlay() {
    override fun draw(pCanvas: Canvas?, pProjection: Projection?) {
        super.draw(pCanvas, pProjection)
    }

    override fun onTouchEvent(event: MotionEvent?, mapView: MapView?): Boolean {
        return super.onTouchEvent(event, mapView)
    }
}

📌좌표

오버레이 기능을 만들려면 좌표에 대해 잘 생각해야 한다. 예를 들어 화면 중심의 좌표를 얻을려면 화면 중심 좌표를 지도 좌표로 변환 해야한다.
osmdroid는 다음 두 메소드를 사용해 좌표를 변환할 수 있다.

fromPixels  toPixels

fromPixels는 화면 좌표를 지도 좌표로 바꿀 수 있다.
pReuse로 Geopoint를 전달하면 결과를 pReuse에 저장해준다.

fromPixels(int x, int y)
fromPixels(final int pPixelX, final int pPixelY, final GeoPoint pReuse)

toPixels는 지도 좌표를 화면 좌표로 바꿀 수 있다.
reuse는 fromPixels와 동일한 기능을 하고, forceWrap는 지도 타일이 반복 설정이면 true가 된다.

toPixels(final IGeoPoint in, final Point reuse)
toPixels(final IGeoPoint in, final Point reuse, boolean forceWrap)

📌좌표 주의사항

지도 회전이 0이면 문제가 없지만, 지도가 회전했으면 좌표 변환이 이상하게 나올 수 있다.
변환이 이상한 이유는 osmdroid에서 좌표에 회전을 적용하기 때문으로 보인다.

MapView는 View를 상속하고 dispatchTouchEvent를 오버라이드하는데, MotionEvent에 회전을 적용한다.

@Override
public boolean dispatchTouchEvent(final MotionEvent event){
	...
	// Get rotated event for some touch listeners.
  MotionEvent rotatedEvent = rotateTouchEvent(event);
	...
}

그리고 View의 dispatchDraw도 오버라이드 하는데, Canvas에 회전을 적용하고 있다.

@Override
protected void dispatchDraw(final Canvas c) {
	...
	// Apply the scale and rotate operations
	getProjection().save(c, true, false);
	...
}

// getProjection().save(c, true, false)
public void save(final Canvas pCanvas, final boolean pMapRotation, final boolean pForce) {
    if (mOrientation != 0 || pForce) {
        pCanvas.save();
        pCanvas.concat(pMapRotation ? mRotateAndScaleMatrix : mUnrotateAndScaleMatrix);
	}
}

📌좌표 해결 방법

따라서 정확한 위치를 구하려면 별도 처리를 해줘야 한다.

📃좌표 변환 메소드

osmdroid는 다음 코드를 사용해 좌표 변환을 보정할 수 있다.
rotateAndScalePoint는 터치 좌표를 화면 좌표로 변환할 때 사용할 수 있다.

/**
 * This will apply the current map's scaling and rotation for a point. This can be useful when
 * converting MotionEvents to a screen point.
 */
public Point rotateAndScalePoint(int x, int y, Point reuse) {
    return applyMatrixToPoint(x, y, reuse, mRotateAndScaleMatrix, mOrientation != 0);
}

unrotateAndScalePoint는 고정된 화면 좌표를 화면에 그릴 때 사용할 수 있다.

/**
 * This will revert the current map's scaling and rotation for a point. This can be useful when
 * drawing to a fixed location on the screen.
 */
public Point unrotateAndScalePoint(int x, int y, Point reuse) {
    return applyMatrixToPoint(x, y, reuse, mUnrotateAndScaleMatrix, mOrientation != 0);
}

다음 방법들은 내가 개발하면서 사용한 방법들인데, 더 좋은 방법이 있을 수도 있으니 참고만 하면 된다.

다음 방법들은 기본적으로 Overlay의 draw와 onTouchEvent 안에서 실행된다.

📃지도 좌표 -> 화면에 그리기

draw에 전달되는 canvas는 회전이 적용된 상태고 toPixels도 내부적으로 회전을 적용하기 때문에 별도 처리가 필요없다.

override fun draw(pCanvas: Canvas?, pMapView: MapView?, pShadow: Boolean) {
    super.draw(pCanvas, pMapView, pShadow)

    if (pCanvas == null || pMapView == null) return

	// 화면 좌표 구하기
    val leftTop = Point()
    pMapView.projection.toPixels(GeoPoint(top, left), leftTop, false)

	// 그리기
    pCanvas.drawCircle(leftTop.x.toFloat(), leftTop.y.toFloat(), 10f, paint)
}

📃화면 좌표 -> 화면에 그리기

길이 200인 정사각형을 화면 중심에 크리는 코드인데, 지도가 회전해도 같은 모양을 보이는 예시다.
osmdroid의 projection에 있는 save는 canvas를 회전시키는 데, 두 번째 파라미터를 false로 전달하면 역회전이 적용된다.
그리기 작업이 완료되면 restore를 반드시 호출해줘야 한다.

override fun draw(pCanvas: Canvas?, pMapView: MapView?, pShadow: Boolean) {
    super.draw(pCanvas, pMapView, pShadow)

    if (pCanvas == null || pMapView == null) return

    val centerX = pCanvas.width / 2f
    val centerY = pCanvas.height / 2f
    val left = centerX - 100
    val right = centerX + 100
    val top = centerY - 100
    val bottom = centerY + 100

    // 이거 안 하면 concat 회전때문에 원하는 형태 안나옴, 두번째 false 중요
    pMapView.projection.save(pCanvas, false, false)
    pCanvas.drawRect(left, top, right, bottom, paint)
    pMapView.projection.restore(pCanvas, false)
}

참고로 다음 방식도 가능하다. 이때는 save를 안해도 되지만 unrotateAndScalePoint를 해줘야한다.

override fun draw(pCanvas: Canvas?, pMapView: MapView?, pShadow: Boolean) {
    super.draw(pCanvas, pMapView, pShadow)

    if (pCanvas == null || pMapView == null) return

    val centerX = pCanvas.width / 2f
    val centerY = pCanvas.height / 2f
    val left = centerX - 100
    val right = centerX + 100
    val top = centerY - 100
    val bottom = centerY + 100

    val leftTop = pMapView.projection.unrotateAndScalePoint(left.toInt(), top.toInt(), null)
    val rightBottom = pMapView.projection.unrotateAndScalePoint(right.toInt(), bottom.toInt(), null)
            
    pCanvas.drawCircle(leftTop.x.toFloat(), leftTop.y.toFloat(), 10f, paint)
    pCanvas.drawCircle(rightBottom.x.toFloat(), rightBottom.y.toFloat(), 10f, paint)
}

📃터치 좌표 -> 화면에 그리기

onTouchEvent에서 터치에 회전을 적용시키기 때문에 별도 처리를 안해도 된다.

override fun onTouchEvent(event: MotionEvent?, mapView: MapView?): Boolean {
    if (event == null || mapView == null) return false
				
	// 부모 클래스에서 MotionEvent에 회전 처리가 되어 있음
    touchPoint.set(event.x.toInt(), event.y.toInt())

    return super.onTouchEvent(event, mapView)
}
    
override fun draw(pCanvas: Canvas?, pMapView: MapView?, pShadow: Boolean) {
    super.draw(pCanvas, pMapView, pShadow)

    if (pCanvas == null || pMapView == null) return

    // 터치와 canvas 둘 다 회전이 적용돼서 별도 처리 안해도 됨
    pCanvas.drawCircle(touchPoint.x.toFloat(), touchPoint.y.toFloat(), 10f, paint)
}

📃터치 좌표 -> 지도 좌표

터치 좌표에 회전이 적용된 상태라 fromPixels를 바로 적용해도 문제 없다.

override fun onTouchEvent(event: MotionEvent?, mapView: MapView?): Boolean {
    if (event == null || mapView == null) return false

    touchPoint.set(event.x.toInt(), event.y.toInt())

    val touchToGeo = mapView.projection.fromPixels(touchPoint.x, touchPoint.y)

    val geoText = "Touch to Geo: ${touchToGeo.latitude}, ${touchToGeo.longitude}"

    return super.onTouchEvent(event, mapView)
}

📃터치 좌표 -> 화면 좌표

터치 좌표에 회전이 적용된 상태라 화면 좌표로 바꾸려면 별도 처리를 해야 한다.

override fun onTouchEvent(event: MotionEvent?, mapView: MapView?): Boolean {
    if (event == null || mapView == null) return false

    touchPoint.set(event.x.toInt(), event.y.toInt())

    // MotionEvent에 회전 처리가 된 상태
	// 별도 처리를 해야 함
    val rotateTouchPoint = mapView.projection.rotateAndScalePoint(
        touchPoint.x,
        touchPoint.y,
        null
    )

    val pointText = "Touch Point: ${rotateTouchPoint.x}, ${rotateTouchPoint.y}"

    return super.onTouchEvent(event, mapView)
}

📌결론

좌표 내용이 대부분이지만, 오버레이에서 좌표만 잘 사용하면 대부분의 기능을 구현할 수 있을 거라 생각한다. 내 경우에는 좌표 변환을 사용해 축척 표시, 마커 표시, 지역 표시 기능 등을 개발했다.

profile
안드로이드 개발자

0개의 댓글