GIS Developer, 공간정보시스템 개발 블로그 -
   GIS Developer, 공간정보시스템 개발 블로그  
Front Page
Notice | E-Mail | Admin | Write Article   
 
2016/07/25 22:54 2016/07/25 22:54
[BlackPoint-Xr] 퍼펙트 튜토리얼 - 14 : 그래픽 요소 편집에 대한 스냅핑(Snapping)

공간 데이터에 대한 편집 시 정점(Vertex)과 선분(Segment)에 대한 스냅핑(Snapping) 기능에 대한 API에 대해 살펴 보겠습니다. 이 스냅핑 기능을 사용자에게 제공하여 편의성과 정확성을 높일 수 있습니다. 이 튜토리얼에서 작성되는 코드는 퍼펙트 튜토리얼 - 11 : 터치를 통한 그래픽 요소 편집 및 Undo / Redo에서 만든 코드를 기반으로 합니다.

스냅핑 기능은 스냅핑 대상이 되는 참조 레이어를 지정해 줘야 합니다. 이렇게 스냅핑 대상이 되는 참조 레이어가 지정되면 사용자가 터치한 위치에 인접한 참조 레이어의 도형에 자석처럼 입력한 위치가 이동하게 됩니다. 스냅핑 대상이 되는 참조 레이어를 지정하기 위해 LayersLoadingTask 클래스의 doInBackground 함수를 다음처럼 수정합니다.

@Override
protected Void doInBackground(Void... args) {
    String ess = Environment.getExternalStorageState();

    if(ess.equals(Environment.MEDIA_MOUNTED)) {
        String sdCardPath =
        Environment.getExternalStorageDirectory().getAbsolutePath();

        String rootDir = sdCardPath + "/mapdata/shp";

        ShapeLayer lyr = new ShapeLayer("TL_SPRD_RW", rootDir + "/TL_SPRD_RW.shp");
        lyr.setKeepShownFID(true);
        layerMan.addLayer(lyr);

        GraphicLayer gl = new GraphicLayer("gl");
        layerMan.addLayer(gl);

        gl.setTopLayer(true);
        editMan.setTargetLayer(gl);

        SnapManager snapMan = editMan.getSnapManager();
        snapMan.addSnapTarget(lyr);
    }

    return null;
}

위의 코드에서 스냅핑 대상이 되는 레이어를 지정하는 코드는 21~22번입니다. 즉, SHP 파일을 통한 실폭 도로 레이어로 지정하였습니다. 참조 레이어를 여러개를 추가하여 지정할 수 있으며 SHP 파일을 통한 ShapeLayer과 GraphicLayer 객체를 지정할 수 있습니다. 참조 레이어를 지정하는 코드 이외에 새롭게 추가된 코드가 하나 더 있습니다. 바로 12번 코드의 setKeepShownFID 함수의 호출입니다. 이 함수를 true 인자를 넘겨 호출하면 해당 레이어에 대한 ID 값에 대한 캐쉬가 적용됩니다. 즉, 화면에 표시되는 도형에 대한 ID 값이 별도로 저장되어 빠르게 스냅핑 연산을 수행할 수 있게 됩니다.

스냅 대상이 되는 참조 레이어를 지정하는 것과 함께 더 해줘야 할 것이 있습니다. 참조 레이어를 구성하는 도형에 대해 스냅핑을 적용하기는 하는데, 이 도형의 정점(Vertex)과 선분(Segment) 중 어디에 스냅핑을 적용할 것인지에 대한 지정이 필요합니다. 이는 UI를 통해 실행시 사용자가 손쉽게 선택할 수 있도록 하겠습니다. 즉, 아래와 같은 UI를 편집 모드에서 표시되도록 할 것입니다.

위의 화면 하단을 보면 Vertex Snap과 Edge Snap이란 제목으로 CheckBox가 있습니다. 이 체크박스의 체크여부에 따라 정점에 대해 스냅핑할 것인지, 선분에 대해 스냅핑할 것인지, 아니면 둘 모두에 대해서 스냅핑 할 것인지를 지정할 수 있습니다. 이러한 UI를 위해서 content_main.xml 파일을 열어 아래처럼 수정합니다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="kr.co.geoservice.blackpoint_xrtutorial14.MainActivity"
    tools:showIn="@layout/activity_main">

    <geoservice.blackpoint.XrMap
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <LinearLayout
        android:id="@+id/layoutSnap"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_marginBottom="26dp"
        android:layout_marginLeft="26dp"
        android:paddingRight="10dp"
        android:paddingLeft="5dp"
        android:visibility="gone"
        android:background="@drawable/round_rect_shape"
        android:orientation="horizontal">

        <CheckBox 
            android:id="@+id/cbVertexSnap" 
            android:layout_height="wrap_content" 
            android:layout_width="wrap_content" 
            android:text="Vertex Snap" />
        
        <CheckBox 
            android:id="@+id/cbEdgeSnap" 
            android:layout_height="wrap_content" 
            android:layout_width="wrap_content" 
            android:text="Edge Snap" />
    </LinearLayout>
</RelativeLayout>

새롭게 추가된 태그는 16~41번입니다. 체크박스를 감싸는 레이아웃의 id를 layoutSnap이라고 지정하여 편집 상태에서만 이 레이아웃을 표시하도록 할 것입니다. 그리고 정점 대상 스냅핑 체크박스의 id는 cbVertexSnap이며 선분 대상 스냅핑 체크박스의 id는 cbEdgeSnap입니다. 체크박스를 감싸는 레이아웃의 배경을 @drawable/round_rect_shape으로 지정되어 있는데요. 이는 이 레이아웃을 하얀색의 둥근박스로 그리기 위한 것으로써 리소스의 drawable 폴더에 round_rect_shape.xml로써 아래와 같습니다.

<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >

    <solid android:color="#ffffff" />

    <corners
        android:bottomLeftRadius="12dp"
        android:bottomRightRadius="12dp"
        android:topLeftRadius="12dp"
        android:topRightRadius="12dp" />

</shape>

자, 이제 위의 체크박스를 감싸는 레이아웃에 대한 표시 여부를 지정해야 합니다. 즉, 편집 모드에서는 보이고 편집 모드가 아니면 보이지 않도록 해야 합니다. 아를 위해 MainActivity의 onOptionsItemSelected 함수를 아래처럼 수정합니다.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();

    if (id == R.id.action_viewMode) {
        map.getEditManager().cancelSketch(false);
        map.setMouseMode(MouseMode.MapViewMode);

        findViewById(R.id.layoutSnap).setVisibility(View.GONE);

        return true;
    } else if (id == R.id.action_editMode) {
        map.setMouseMode(MouseMode.EditMode);

        findViewById(R.id.layoutSnap).setVisibility(View.VISIBLE);

        return true;
    } else if (id == R.id.action_addPoint) {
        ....

추가된 코드는 9번과 15번으로, 편집 모드로 전환하는 메뉴를 실행할 때와 뷰 모드로 전환하는 메뉴를 실행할 때에 layoutSnap 레이아웃의 표시 여부를 지정하고 있습니다. 이제 체크 박스에 대해 사용자가 체크 여부를 변경할 때 발생하는 이벤트를 지정해야 합니다. MainActivity의 onCreate 함수를 아래처럼 수정합니다.

@Override
protected void onCreate(Bundle savedInstanceState) {
    
    ....

    CheckBox cbEdgeSnap = (CheckBox)findViewById(R.id.cbEdgeSnap);
    cbEdgeSnap.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            map.getEditManager().getSnapManager().setSegmentSnapMode(isChecked);
        }
    });

    CheckBox cbVertexSnap = (CheckBox)findViewById(R.id.cbVertexSnap);
    cbVertexSnap.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            map.getEditManager().getSnapManager().setVertexSnapMode(isChecked);
        }
    });
}

각 체크박스의 체크 여부에 따라 setSegmentSnapeMode와 setVertexSnapMode 함수를 통해 도형의 어떤 부분에 스냅핑을 활성화할지를 지정하고 있습니다.

이제 실행을 하고 편집 모드에서 Vertex Snap만을 활성화한 후 새로운 폴리라인을 실폭 도로를 따라 그려보기 바랍니다. 그러면 아래의 화면처럼 실폭 도로를 구성하는 정점에 사용자가 입력한 터치 위치가 최대한 붙어 입력되게 됩니다.

2016/07/25 15:21 2016/07/25 15:21
[BlackPoint-Xr] 퍼펙트 튜토리얼 - 13 : 그래픽 요소에 대한 공간 연산자(SpatialOperator)

그래픽 요소 중 Point, Polyline Polygon은 GIS에서 일반적인 Geometry로써 다양한 공간 연산 기능을 수행할 수 있습니다. 블랙포인트에서 Geometry에 대해 지원되는 공간 연산은 ISpatialOperator이라는 인터페이스를 통해 제공되며, 연산의 종류는 아래와 같습니다.

buffer, contains, convexHul, coveredBy, covers, crosses, difference, disjoint, touches
distance, equals, equalsExact, equalsNorm, equalsTopo, getArea, getBoundary, union
getCentroid, getMBR, getInteriorPoint, getLength, intersection, intersects, 
isEmpty, isSimple, isValid, isWithinDistance, norm, normalize, overlap, reverse, within

위의 연산자 중 이 튜토리얼에서는 GIS에서 가장 재밌다고 생각되는 buffer 연산자를 사용해 보겠습니다. buffer 연산자는 주어진 반경만큼 선택된 도형을 부풀리거나 축소시키는 연산입니다.

이 튜토리얼의 코드는 퍼펙트 튜토리얼 - 11 : 터치를 통한 그래픽 요소 편집 및 Undo / Redo 에서 작성된 코드를 기반으로 합니다.

buffer 연산 대상이 될 그래픽 도형이 선택되면 buffer 연산을 실행할 반경값을 입력받는 대화상자를 표시하고, 이를 통해 사용자가 입력한 반경값만큼 Geometry를 확장하거나 축소합니다. 반경값이 양수이면 확장되고, 음수이면 축소됩니다. 이를 위해서 먼저 buffer 연산 대상이 될 그래픽 도형이 선택되는 시점을 잡아야 합니다. 이는 그래픽 요소가 선택될때 호출되는 이벤트 함수인 onEditingSelectionChangedEvent에서 확인이 가능합니다. 이 함수에 대해 기존의 코드를 모두 제거하고 아래의 코드로 변경합니다.

@Override
public void onEditingSelectionChangedEvent(EditingSelectionChangedEvent event) {
    int id = event.getFirstFID();
    if(id != -1) {
        GraphicLayer gl = (GraphicLayer)map.getLayerManager().getLayerByName("gl");

        try {
            final GraphicRow row = gl.getGraphic(id);
            int gt = row.getGraphic().getGraphicType();
            boolean bBufferEnable = (
                gt == Graphic.GRAPHIC_TYPE_POINT ||
                gt == Graphic.GRAPHIC_TYPE_POLYGON ||
                gt == Graphic.GRAPHIC_TYPE_POLYLINE
            );

            if(!event.isNewItem() && bBufferEnable) {
                AlertDialog.Builder alert = new AlertDialog.Builder(this);

                alert.setTitle("Input Buffer Radius Distance");

                final EditText etBuffer = new EditText(this);
                etBuffer.setTextAlignment(EditText.TEXT_ALIGNMENT_CENTER);
                alert.setView(etBuffer);

                alert.setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                        String bufferValue = etBuffer.getText().toString();
                        double radius = Double.parseDouble(bufferValue);
                        ISpatialOperator op = row.toSpatialOperator();

                        if(op != null && op.buffer(radius, 4)) {
                            ArrayList<ArrayList<PointD>> polygons = new ArrayList<>();
                            PolygonGraphic newGrp = new PolygonGraphic(polygons);
                            GraphicRow newRow = new GraphicRow(graphicId++, newGrp);

                            newGrp.getFillSymbol().setColor(Color.RED).setAlpha(125);
                            newGrp.getStrokeSymbol().setWidth(6).setColor(Color.YELLOW);

                            if(newRow.fromSpatialOperator(op)) {
                                GraphicLayer gl = 
                                    (GraphicLayer)map.getEditManager().getTargetLayer();
                                gl.addGraphic(newRow);

                                map.getEditManager().cancelSketch(false);
                                map.refresh();
                            }
                        } else {
                            Toast.makeText(MainActivity.this, 
                                "SpatialOperation Fail", Toast.LENGTH_LONG).show();
                        }
                    }
                }).setNegativeButton("Cancel", null);

                alert.show();
            }
        } catch (IOException e) {
            //.
        }
    }
}

주의해야할 코드만을 설명하면, buffer 연산은 Point, Polyline, Polygon에 대해서만 가능합니다. 이를 위해 10번 코드에서 현재 선택된 그래픽 요소의 종류가 이에 해당하는지에 대한 값을 저장해 놓고, 16번의 if문 코드에서 사용합니다. 이 if문을 보면 event.isNewItem()이 false일 경우를 검사합니다. 이는 신규 생성으로 인해 자동으로 선택되었을 경우는 buffer 연산을 실행하지 않도록 하기 위함입니다. buffer 연산이 가능하다면 사용자에게 AlertDialog UI를 통해 버퍼 연산의 반경값을 아래의 화면처럼 입력받습니다.

사용자가 반경값을 입력하고 OK 버튼을 터치하면, 27~48번 코드가 실행됩니다. 27~28번 코드는 사용자가 입력한 버퍼 반경값을 double 타입으로 변환합니다. 그리고 29번 코드는 공간연산자 함수를 실행할 수 있도록 그래픽 요소를 ISpatialOperator 인터페이스 타입으로 변환합니다. 31번 코드에서 실제 buffer 연산자에 대한 함수를 실행하는데요. 이 buffer 함수는 2개의 인자를 갖습니다. 첫번째는 앞서 사용자가 입력한 버퍼 반경이고 2번째는 버퍼 연산시 변곡점에서 삽입할 정점의 개수값입니다. 대략적인 모양을 위해 4를 지정했습니다. 32~37번은 버퍼 연산을 통해 생성된 결과를 다시 그래픽 요소로 전환하기 위해 필요한 그래픽 요소에 대한 Row를 만들어 놓는 코드입니다. 이렇게 만들어 놓은 Row에 좌표값을 담기 위해 39번 코드의 fromSpatialOperator 함수를 사용합니다. 43번 코드는 그래픽 요소에 대한선택 표시를 제거 하는 것입니다. 실행하여 Point 그래픽 요소 1개와 Polyline 그래픽 요소 1개, Polygon 그래픽 요소 1개를 추가하여 이 3개에 대해 각각 선택해 buffer 연산을 실행하면 다음과 같은 결과를 볼 수 있습니다.

BLOG main image
GIS Developer, 김형준 / '모바일 3D 그래픽스' 번역 및 출판 / '모바일 GIS 프로그래밍' 집필 및 출판
 Notice
 Category
전체 (773)
GIS 개발 (243)
공간DB 공유 (3)
프로그래밍 (299)
스치는 생각들 (147)
번역 또는 집필 (4)
 TAGS
GIS Xr OpenGL Shader BlackPoint FingerEyes C++ Algorithm Android Java DuraMap Map Engine WPF ActionScript C# ArcObjects ArcGIS 안드로이드 FingerEyes-Xr BlackPoint-Xr JavaScript HTML5 WPF 3D template Flex DuraMap-Xr OrangeMap Pattern Service .NET Graphic OpenGL ES XML Mr.Tiler-Xr OOD Mobile GIS XGE WebService Design GPS GIS Korea PSP KASS WKT PostGIS GeoService iOS 모바일 GIS Edit Web
 Recent Entries
[BlackPoint-Xr] 퍼펙트 ...
[BlackPoint-Xr] 퍼펙트 ...
[BlackPoint-Xr] 퍼펙트 ...
[BlackPoint-Xr] 퍼펙트 ...
[BlackPoint-Xr] 퍼펙트 ...
 Archive
2016/07
2016/06
2016/05
 Visitor Statistics
Total : 4200437
Today : 161
Yesterday : 1381
태터툴즈 배너
rss