[Android] 화면 터치 중 Swiping을 이용한 View 전환

화면 터치가 가능한 모바일 단말기에서 터치를 통한 UI의 조작은 매우 효과적입니다. 이러한 터치 기반의 UI의 활용에 대해 자연스러운 사용은 사용자에게 프로그램의 친밀도를 높여줍니다. 화면 터치에 대한 조작 중 Swiping은 사용자가 화면을 스치듯이 상하좌우로 쓸어넘기는 행위입니다. 이러한 Swiping 중 좌우에 대한 이벤트를 처리하기 위한 클래스는 다음과 같습니다.

package geoservice.nexgen

import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View

abstract class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
    companion object {
        private const val SWIPE_DISTANCE_THRESHOLD = 100
        private const val SWIPE_VELOCITY_THRESHOLD = 100
    }

    private val gestureDetector: GestureDetector

    abstract fun onSwipeLeft()
    abstract fun onSwipeRight()

    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
        return gestureDetector.onTouchEvent(event)
    }

    private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
        override fun onDown(e: MotionEvent): Boolean {
            return true
        }

        override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
            val distanceX = e2.x - e1.x
            val distanceY = e2.y - e1.y
            if (Math.abs(distanceX) > Math.abs(distanceY) 
                    && Math.abs(distanceX) > SWIPE_DISTANCE_THRESHOLD 
                    && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
                if (distanceX > 0) onSwipeRight() else onSwipeLeft()
                return true
            }
            return false
        }
    }

    init {
        gestureDetector = GestureDetector(context, GestureListener())
    }
}

위 클래스를 실제 View에 적용하는 코드의 예는 다음과 같습니다.

llListScroll.setOnTouchListener(object: OnSwipeTouchListener(context) {
    override fun onSwipeLeft() { btnNext.performClick() }
    override fun onSwipeRight() { btnPrevious.performClick() }
})

실제 위의 코드는 모바일 기반의 GIS 솔루션인 Mobile NexGen에 반영된 코드인데요. 위의 코드와 연관된 기능에 대한 시연 영상은 아래와 같습니다.

잘못된 Geometry를 수정하기

PostGIS의 공간 함수 중 ST_MakeValid는 이미 저장된 올바르지 못한 Geometry를 수정해 준다. 아래의 쿼리는 이에 대한 내용이다.

select ST_IsValid(the_geom) from sify_li;
update sify_li set the_geom = ST_MakeValid(the_geom);

위의 SQL 구문 중 sify_li는 Table 이름이며 the_geom은 Geometry 타입에 대한 필드명에 대한 예시이다.

멀티 폴리곤의 경우 실패하는 경우가 있는데 아래의 SQL문을 사용하면 해결되는 경우가 있음

update sig set the_geom = st_multi(st_collectionextract(st_makevalid(the_geom),3)) where not st_isvalid(the_geom);

FingerEyes-Xr을 이용한 HeatMap 시각화

웹 GIS 엔진인 FingerEyes-Xr은 밀도도를 시각화하기 위한 방법 중의 하나인 HeatMap 기능을 제공합니다. 바로 Xr.layers.HeatMapLayer 클래스를 통해 수행이 가능합니다. HeatMap이 아닌 또 다른 밀도도에 대한 시각화 방법은 Xr.layers.GridLayer 클래스를 사용하는 것인데, Xr.layers.GridLayer는 셀 기반의 연산을 이용해 밀도도 분석을 수행합니다. 좀더 정밀한 밀도도 분석은 Xr.layers.GridLayer이 Xr.layers.HeatMapLayer보다 우수하지만 연산 시간은 Xr.layers.HeatMapLayer가 훨씬 빨라 실시간으로 밀도도를 시각화할 수 있다는 장점을 갖습니다.

아래의 영상은 FingerEyes-Xr의 Xr.layers.HeatMapLayer를 사용하여 생성된 밀도도입니다.

이에 대한 코드는 다음처럼 간단 명료합니다.

let heatMapLayer = map.layers('heatmap');
if (!heatMapLayer) {
    heatMapLayer = new Xr.layers.HeatMapLayer('heatmap');
    map.layers().add(heatMapLayer);
}
            
heatMapLayer.generateByLayer(lyr);

밀도도 분석을 위한 입력 데이터로 공간상에 분포된 포인트 좌표들이 필요하며, 이 포인트 좌표는 7번 코드의 lyr 변수명의 레이어를 통해 입력됩니다.

Xr.layers.HeatMapLayer는 밀도도 분석이 매우 빨라 실시간으로 밀도도를 생성할 수 있습니다. 아래의 코드는 입력 데이터가 변경되는 즉시 밀도도를 새롭게 생성해 표시합니다.

map.addEventListener(Xr.Events.LayerUpdateCompleted, function (e) {
    if (e.layerName === lyr.name()) {
        let heatMapLayer = map.layers('heatmap');
        if (heatMapLayer) {
            heatMapLayer.generateByLayer(lyr); 
        }
    }
});

끝으로 보다 정밀한 밀도도를 생성하기 위한 또다른 방식인 Xr.layers.GridLayer에 대한 실제 활용예는 아래의 글을 참고하기 바랍니다.

NexGen, 공간 데이터의 분포경향 분석을 위한 밀도맵 기능

FingerEyes-Xr의 편집 이벤트

FingerEyes-Xr의 공간 데이터 편집시 발생하는 이벤트는 1개입니다. Xr.Events.EditingCompleted로 사용자가 선택한 도형을 편집한 뒤에 발생하는 이벤트입니다. 등록은 다음과 같습니다.

map.addEventListener(Xr.Events.EditingCompleted, onMapEditingCompleted);

그리고 이벤트에 대한 콜백 함수인 onMapEditingCompleted 함수는 아래와 같이 작성할 수 있습니다.

function onMapEditingCompleted (e) 
{
    let map = e.map;
    let type = e.editCommandType;
    let rowId = e.rowId;

    if(type === Xr.edit.AddPartCommand.TYPE) {
        // 여러 개의 요소를 갖는 도형에 대해 1개의 새로운 요소가 추가될 때 ...
    } else if(type === Xr.edit.AddVertexCommand.TYPE) {
        // 도형에 대해 정점이 하나 추가될 때 ...
    } else if(type === Xr.edit.MoveCommand.TYPE) {
        // 도형 전체가 이동될 때 ...
    } else if(type === Xr.edit.MoveControlPointCommand.TYPE) {
        // 도형의 제어점이 이동되었을 때 ...
    } else if(type === Xr.edit.NewCommand.TYPE) {
        // 새로운 도형이 생성되었을 때 ...
    } else if(type === Xr.edit.RemoveCommand.TYPE) {
        // 기존의 도형을 제거했을 때 ...
    } else if(type === Xr.edit.RemovePartCommand.TYPE) {
        // 도형을 구성하는 하나의 요소를 제거했을 때 ...
    } else if(type === Xr.edit.RemoveVertexCommand.TYPE) {
        // 도형을 구성하는 정점을 제거했을 때 ...
    }
}

이벤트 함수로 넘겨지는 이벤트 객체인 e의 map은 편집이 이루어진 지도 객체를 의미하며, rowId는 편집 대상이 되는 Row의 ID 값입니다. 그리고 editCommandType은 위의 코드의 if 문에서 언급한 주석의 내용일 때를 파악하기 위해 사용됩니다.

추가로, 위의 편집 이벤트를 위해 선행되어야할 것은 도형에 대한 편집 행위의 시발을 발생해줘야 한다는 것입니다. 아래는 그래픽 레이어에 사각형을 새롭게 생성하는 것에 대한 편집의 시작 코드입니다.

let gl = new Xr.layers.GraphicLayer("gl_community");

map.layers().add(gl);
map.edit().targetGraphicLayer(gl);

map.userMode(Xr.UserModeEnum.EDIT);
map.edit().newRectangle(0);

마지막 코드에서 newRectangle 함수의 인자값인 0은 새롭게 생성할 도형이 가질 id 값입니다.

만약 새로운 도형이 추가되면 onMapEditingCompleted 이벤트의 Xr.edit.NewCommand.TYPE 조건에 걸리게 되는데, 이 조건에서 추가된 도형의 상세 정보를 얻기 위한 코드 예시는 다음과 같습니다.

function onMapEditingCompleted(e) {
    let map = e.map;
    let type = e.editCommandType;
    let rowId = e.rowId;
            
    if ( ... ) {
        ...
    } else if (type === Xr.edit.NewCommand.TYPE) {
        let row = map.edit().targetGraphicLayer().row(rowId);
        let data = row.graphicData().data();

        if (data instanceof Xr.data.RectangleShapeData) {
            console.log("RECTANGLE:", data.minX, data.minY, data.maxX, data.maxY);
        } else if (data instanceof Xr.data.EllipseShapeData) {
            console.log("ELLIPSE:", data.cx, data.cy, data.rx, data.ry);
        } else if (data instanceof Xr.data.PointShapeData) {
            console.log("POINT:", data.x, data.y);
        } else if (data instanceof Xr.data.PolylineShapeData) {
            let cntParts = data.length;
            console.log("POLYLINE:");
            for (let iPart = 0; iPart < cntParts; iPart++) {
                let part = data[iPart];
                let cntVtx = part.length;
                for (let iVtx = 0; iVtx < cntVtx; iVtx++) {
                    console.log(part[iVtx].x + ", " + part[iVtx].y);
                }
            }
        } else if (data instanceof Xr.data.PolygonShapeData) {
            console.log("POLYGON:");
            let cntParts = data.length;
            for (let iPart = 0; iPart < cntParts; iPart++) {
                let part = data[iPart];
                let cntVtx = part.length;
                for (let iVtx = 0; iVtx < cntVtx; iVtx++) {
                    console.log(part[iVtx].x + ", " + part[iVtx].y);
                }
            }
        }
    } else if ( ... ) {
        ...
    }
}

마지막으로 위의 편집 이벤트가 적용된 실제 편집 기능에 대한 동영상은 아래와 같습니다.