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