[GIS] GeoTools를 이용해 SHP, DBF 파일 읽기

GeoTools(http://www.geotools.org)는 자바 기반의 GIS 시스템을 구축할 수 있는 다양한 기능을 갖춘 오픈소스입니다. 제가 처음 GeoTools에 관심을 가졌던 이유는 SHP 파일을 읽기 위한 자바 라이브러리가 필요해서 였는데요. 이 기능을 파악하기 위해 테스트로 작성했던 코드를 공유해 봅니다.

먼저 SHP 파일에서 좌표를 읽어 들이는 코드입니다.

import java.io.IOException;
import java.net.MalformedURLException;

import org.geotools.data.shapefile.shp.ShapefileException;
import org.geotools.data.shapefile.shp.ShapefileReader;
import org.geotools.data.shapefile.shp.ShapefileReader.Record;
import org.geotools.data.shapefile.ShpFiles;

import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;

public class ShapefileReaderTestMainEntry {
    public static void main(String[] args) {
        ShapefileReader r = null;
        try {
            ShpFiles shpFile = new ShpFiles("G:\\__Data__\\dong.shp");

            GeometryFactory geometryFactory = new GeometryFactory();
            r = new ShapefileReader(shpFile, true, false, geometryFactory);

            while (r.hasNext()) {
                Record record = r.nextRecord();
                Geometry shape = (Geometry)record.shape();
                Point centroid = shape.getCentroid();
                System.out.println(
                    "(" 
                    + centroid.getX() 
                    + ", " 
                    + centroid.getY() 
                    + ")"
                );
            }
            r.close();
        } catch (MalformedURLException e1) {
            e1.printStackTrace();   
        } catch (ShapefileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        };
    }
}

다음으로는 DBF 파일에서 값을 읽어 들이는 코드입니다.

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.charset.Charset;

import org.geotools.data.shapefile.ShpFiles;
import org.geotools.data.shapefile.dbf.DbaseFileHeader;
import org.geotools.data.shapefile.dbf.DbaseFileReader;

public class DbaseFileReaderTestMainEntry {
    public static void main(String[] args) {
        DbaseFileReader r = null;
        try {
            ShpFiles shpFile = new ShpFiles("G:\\__Data__\\dong.shp");
            r = new DbaseFileReader(shpFile, false, Charset.defaultCharset());
            DbaseFileHeader header = r.getHeader();

            int numFields = header.getNumFields();
            for(int iField=0; iField < numFields; ++iField) {
                String fieldName = header.getFieldName(iField);
                System.out.println(fieldName);
            }

            while (r.hasNext()) {
                Object[] values = r.readEntry();
                for(int iField=0; iField < numFields; ++iField) {
                    System.out.println(values[iField].toString());
                }
                System.out.println("---------------");
            }

            r.close();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }  
    }
}

[C++] XML 파서, CMarkup

XML 데이터를 쓰고 읽기 위해서 인터넷을 검색하던 차에 속도를 강점으로 내세우면서 STL 만을 사용하여 플랫폼 이식에도 뛰어난 오픈소스를 찾았는데요. 바로 CMarkup 입니다. 다운로드 사이트는 http://www.firstobject.com/ 이구요. 사용해 보니 XML의 charset도 지원하여 더욱 믿음이 가는 오픈소스였습니다. XML을 처리할 일이 있다면 한번 사용해 보시기 바랍니다.

추후 이 오픈소스를 다시 사용할 때를 대비하여 사용 방법을 정리해 정리차원에서 올려봅니다.

컴파일 시 주의할 사항은… CMarkup은 MFC의 CString와 STL의 string에 대한 문자열 타입을 사용하며 기본적으로 CString을 사용합니다. 플랫폼에 독립적인 구성을 위해서 STL을 사용하는것이 좋기 때문에 전처리에서 MARKUP_STL를 정의해줘야 합니다. 이 전처리 정의는 프로젝트의 속성 페이지에서 해줌으로써 전역적으로 적용되도록 해야 합니다. 아래의 코드는 XML을 쓰는 예제 코드입니다. 윈도우즈 계열의 개발툴인 VS2008로 작성했습니다.

#include "stdafx.h"
#include "../Markup.h"

int _tmain(int argc, _TCHAR* argv[])
{
    CMarkup xml;

    xml.AddElem( "ORDER" );
    xml.AddChildElem( "ITEM" );
    xml.IntoElem();
    xml.AddAttrib("type", "A");
    xml.AddChildElem( "SN", "132487A-J" );
    xml.AddChildElem( "NAME", "crank casing" );
    xml.AddChildElem( "QTY", "1" );
    xml.OutOfElem();

    xml.AddChildElem( "ITEM" );
    xml.IntoElem();
    xml.AddAttrib("type", "C");
    xml.AddChildElem( "SN", "434417F-Y" );
    xml.AddChildElem( "NAME", "kully casing" );
    xml.AddChildElem( "QTY", "2" );
    xml.OutOfElem();

    std::string csXML = xml.GetDoc();

    printf("%s", csXML.c_str());

    return 0;
}

결과는 다음과 같습니다.

사용자 삽입 이미지
AddElement를 통해 엘리먼트를 만들고, 해당 엘리먼트의 자식을 추가하기 위해 AddChildElem을 사용하거나 먼저 IntoElem을 호출한 후 다시 AddElement를 사용합니다.

이제 반대로 위와 같은 XML 데이터를 읽어 보도록 하겠습니다. 먼저 위의 결과를 파일로 저장해 놓고 그 파일을 읽어 데이터를 추출하는 예제를 들어 보겠습니다. 예를 들어… 아이템(ITEM)의 이름(NAME)과 수량(QTY)을 읽어 보도록 하겠습니다. 위의 XML 문자열의 경우 아이템의 개수는 총2개이므로 2개가 검색될 것입니다. 다음이 이 예제와 부합되는 코드입니다.

CMarkup xml;
xml.Load("d:/data.xml");

while ( xml.FindChildElem("ITEM") )
{
    xml.IntoElem();

    xml.FindChildElem( "NAME" );
    std::string csSN = xml.GetChildData();
    xml.FindChildElem( "QTY" );
    int nQty = atoi(xml.GetChildData().c_str());

    xml.OutOfElem();

    printf("%s, %d\n", csSN.c_str(), nQty);
}

먼저 XML 데이터를 가지고 있는 파일을 Load 매서드를 이용해 읽습니다. 다시 반복문을 사용하여 루트 엘리먼트의 자식 ITEM 엘리먼트를 검색하기 위해 FindChildElem을 사용합니다. 그러면 해당되는 자식 엘리먼트가 추출됩니다. 해당되는 자식 엘리먼트의 자식 엘리먼트를 읽기 위해 IntoElem() 함수를 사용한 뒤에 원하는 엘리먼트(NAME, QTY)를 검색하기 위해 FindChildElem을 호출하고 실제로 값을 읽기 위해서 GetChildData 매서드를 호출합니다. 다 읽은 후 OutOfElem()을 호출합니다. 결과는 아래와 같습니다.

사용자 삽입 이미지

ExampleFrameListener의 개념

ExampleFrameListener는 ExampleApplication 클래스와 마찬가지로 Ogre에서 기본적으로 제공하는 Framework 클래스 중 하나이다. KeyListener와 FrameListener로부터 다중상속을 받는 구조이며 기본적으로 갖는 책임 중에 가장 중요한 것은 마우스나 키보드로부터 입력을 받아 처리하고 Frame을 렌더링하데는 필요한 상태정보를 갱신하고, 렌더링을 계속할 것인지의 여부의 결정이다. 아래는 ExampleFrameListener 클래스 관계도이다.

또한 Overlay 클래스를 이용하여 FPS 정보 등을 나타내준다. 사용자가 마우스나 키보드의 입력을 buffer 방식인지, unbuffered 방식인지의 결정에 따라 buffered 방식인 경우, 사용자 입력에 대한 처리를 Event 방식(Callback 방식)으로 하기 위해 EventProcessor 클래스의 인스턴스를 생성해준다.

unbuffered 방식인 경우에는 InputReader 클래스의 인스턴스를 생성해서 사용자가 필요한 시점에서 InputReader의 인스턴스를 이용해서 키보드나 마우스의 상태를 Query할 수 있다. buffer 방식인 경우에도 InputerReader 인스턴스를 사용할 수 있다는 점도 알아 두길 바란다.

기본적으로 ExampleFrameListener 클래스는 unbuffered 방식의 경우에 대해 마우스와 키보드 입력에 대한 처리를 해 놓았다. 예를 들어 커서키를 누르면 화면이 이동한다든지, 마우스를 이용해 확대나 이동을 한다든지 말이다. 하지만 사용자가 원할 경우 이러한 처리를 재정의해서 쉽게 변경할 수 있다.

buffered 방식인지, unbuffered 방식인지의 결정은 ExampleFrameListener 클래스 생성자의 인자를 통해 결정한다.

사용자 입력 이외에 ExampleFrameListener 클래스에서 중요한 것은 FrameListener 클래스의 상속에 있다. FrameListener 클래스는 단지 frameStarted와 frameEnd 가상함수만을 가지고 있다. frameStarted는 하나의 frame을 렌더링 하기 직전에 호출하고 frameEnd는 하나의 frame에 대한 렌더링이 끝났을때 호출된다. 두개의 함수 모두 bool을 반환하는데, false일 경우 다음 frame을 렌더링하지 않는다. 즉, 계속 렌더링하려면 true를 반환해야 한다.

위의 말대로라면 Ogre3D의 main loop인 Root::startRendering 함수의 내용은 다음과 같을 것이고, 실제로 같다.

  1. Root 객체는 자신에 등록된 모든 FrameListener 객체의 frameStarted 함수를 호출
  2. Root 객체는 하나의 frame을 렌더링 함
  3. Root 객체는 자신에 등록된 모든 FrameListener 객체의 frameEnd 함수를 호출

입력이 unbuffered 방식일때 frameStarted나 frameEnd 함수안에서 InputReader 인스턴스를 이용해 키보드나 마우스의 상태를 Query 해서 회전상태나, 이동상태값들을 변경해서 물체를 회전시키고 이동시킬 수 있다. 클래스 관계도에서 ExampleFrameListener 클래스가 Camera 클래스와 관계를 맺고 있는 이유가 여기에 있는데, 입력에 따라 상태정보를 변경하고 다시 이 상태정보를 통해 Camera를 제어하기 위해서이다. RenderWindow와 연관을 맺는 이유는 다음과 같다.

  • FPS를 얻기
  • 개발을 위한 Debug 기능을 이용
  • 화면을 캡쳐화여 png 파일로 저장

마지막으로, 여러분들이 ExampleFrameListener 클래스의 소스를 열어놓고 기본적으로 입력에 대해서 구현해 놓은 것이 무엇인지를 살펴보길 바란다. 이 글을 읽은 후에 소스를 살펴보면 조금이라도 이해가 수월할 것이다.

ExampleApplication의 개념

ExampleApplication 클래스는 Ogre에서 기본적으로 제공하는 Framework 클래스이다. 이 클래스가 Framework가 될 수 있는 이유는 이 클래스를 상속 받아 가상함수를 구현하면 하나의 3D 어플리케이션이 만들어지기 때문이다. ExampleApplication 클래스의 구조와 흐름을 파악한다면, Ogre의 전체적인 흐름까지 쉽게 파악할 수 있을 것이므로 Ogre의 첫번째 분석에 ExampleApplication에 대해서 집중적으로 살펴보았다. 아래는 ExampleApplication의 클래스 관계도이다. 매우 심플한 구조인것을 알 수 있다. 심플하다는 것은 확장성이 뛰어나다는 것을 의미하며 또 그만큼 견고하다는 의미라고 생각된다. (너무 Ogre를 좋게만 보는거 아닌가 싶다)

위의 클래스 관계도에서 파악할 수 있는 몇가지를 살펴보면, ExampleApplication 클래스는 RenderWindow, Root, SceneManager, FrameListener, Camera를 속성맴버로 갖고 있다. (SceneNode는 아니다) 생성과 소멸에 관한 Composition 관계는 ExampleApplication은 Root와 FrameListener를 생성하며 Root가 RenderWindow와 SceneManager를 생성한다. 그리고 Camera는 SceneManager가 생성하여 관리한다. 일단 여기서 가장 중요한 FrameListener와 SceneManager에 대해 간단히 설명하면, FrameListener는 사용자의 Interaction에 대해 렌더링될 각 Frame을 컨트롤하는 역활을 한다. 그리고 SceneManager는 화면상에 렌더링될 SceneNode 들을 관리해준다.

SceneNode는 직접적으로 ExampleApplication과는 관계가 없지만 Ogre에서 매우 중요한 개념(대부분의 3D엔진에서 중요한 용어이며 개념이다)이다. Ogre에서 SceneNode를 말하려면 Entity도 함께 언급을 해야한다. Entity는 3차원 Geometry 좌표로 구성한 Mesh라고 할 수 있는데, 이 Entity가 SceneNode에 붙게된다. SceneNode는 위치와 방향 값을 갖고 있으므로 Entity와 SceneNode가 붙게되면 특정 위치와 방향으로 놓이게 되는 물체가 정의되는 것이다. 여기서 중요한 것은 하나의 SceneNode에는 여러개의 Entity가 붙을 수 있다는 점이다. 참고로 빛(Light)도 SceneNode에 붙일 수 있다.

관계도에서 하나 더 알 수 있는 것은 ExampleApplication의 모든 맴버 함수는 추상함수라는 점이다. 즉, 앞서 언급한것처럼 ExampleApplication을 상속받아 추상함수를 구현하게 되면 하나의 어플리케이션이 만들어진다는 것이다.

이제 ExampleApplication의 추상함수들의 용도를 살펴보면, 유일한 공개(public) 함수인 go()와 보호(protected) 접근자 함수인 그외의 함수들로 구성되며 그 목적은 다음과 같다.

  • go – WinMain이나 main, 또는 Rendering Thread에서 직접 호출되며 SceneNode 등과 같은 것을 화면상에 렌더링 한다. 내부적으로 setup 함수를 호출하고 조건무한루프 함수인 Root의 startRendering 함수를 실행한 뒤, destroyScene 함수를 호출한뒤, 일반적으로 어플리케이션이 종료된다.
  • setup – 가장 많은 일을 하는 함수로, 리소스 파일을 읽어들이고 Root 클래스를 초기화하며 Root 클래스를 통해 SceneManager를 생성시킨다. 그외에 카메라, 뷰포트, SceneNode를 생성을 담당하는 함수등을 차례로 호출한다.
  • destroyScene – 어플리케이션이 종료되기 직전에 더 이상 필요하지 않을 SceneNode를 메모리에서 제거한다.
  • setupResources – 리소스(이미지, Mesh, 스크립트 등)의 경로를 외부 cfg로부터 읽어들여 추후에 읽어들일 리소스의 경로를 파악한다.
  • configure – 사용자에게 3D API로 OpenGL이나 Direct3D를 쓸것인지와 해상도, 전체화면여부등을 결정할 수 있도록 하고, 결정된 사항을 기반으로 3D 환경을 설정한다.
  • chooseSceneManager – sceneManager를 생성한다.
  • createCamera – 카메라 생성
  • createViewport – Viewport 생성. Viewport는 실제 렌더링될 창(Window)의 배경색 및 픽셀 너비와 높이 값등을 가지고 있다.
  • createResourceListener – 리소스를 실제로 Loading할때 사용자에게 현재 얼마만큼의 리소스가 Loading되었는지를 파악할 수 있는 Listener 클래스를 생성한다.
  • loadResources – 실제 리소스를 Loading 한다.
  • createScene – 다른 추상 함수와는 달리 순수 추상함수로써 화면상에 렌더링할 SceneNode를 생성한다.
  • createFrameListener – 사용자의 Interaction을 받거나 그외의 다른 이유를 통해 변경된 사항을 매 Frame에 반영할 수 있도록하는 Listener를 생성한다. Ogre를 분석때 처음에 ExampleApplication을 분석하는 것처럼 다음에는 ExampleFrameListener를 분석해 볼것이다. 일단 여기서는 간단이 앞서 언급한 내용만 알아도 충분할 것같다.

일단 ExampleApplication에 대한 개념적인 것을 정리했다. 다음에는 실제로 ExampleApplication을 상속받아 화면상에 하나의 모델을 나타내는 예제를 만들어보는 것으로 해서 ExampleApplication에 대한 정리를 마루리할 것이다.

여기서 ExampleApplication의 이름을 ExampleApplication이라고 정의했는지 생각해 볼 필요가 있을 것 같다. ExampleApplication은 단지 Ogre에서 최소한 실행되는 3D 프로그램이 갖추어야할 것들만을 가지고 있는 예제에 불과한것같다. 즉, 처음 Ogre에 접근하는 개발자들에게 ExampleApplication을 상속받아, 얼마나 쉽게 3D 프로그램을 개발할 수 있는지를 보여주기 위한 Example에 불과한것이 아닌가 싶다. 물론 예상일뿐이지만 말이다.