GIS 엔진을 이용한 공간 통계 데이터 시각화 확장

통계 데이터를 공간 상에 시각화하기 위해 지리정보시스템(GIS)을 활용하는 것은 매우 효과적인 방법입니다. 흔히 주제도(Thematic Map)라고 하는 단계색상구분도(Choropleth Map), 차트맵(Chart Map), 밀도맵(Densit Map) 등이 가능하여, 각각의 예시는 아래 그림과 같습니다.

이외에도 다양한 종류의 주제도가 있고, 표현하고자 하는 관점에서 새로운 주제도가 계속 생겨날 것입니다. 이에 대해 GIS 엔진을 이용하여 새로운 주제도를 생성하는 내용을 API 관점에서 정리해 봅니다. GIS 엔진에 대한 정의는 다양하지만, 여기서 언급하는 GIS 엔진은 클라이언트 관점에서 지도를 시각화하고 지도를 조작하는 기능 등을 API로 제공하는 프로그램입니다. 이러한 GIS 엔진 중 저희 회사에서 개발한 FingerEyes-Xr을 이용해 글을 작성합니다.

FingerEyes-Xr은 통계 데이터를 공간 데이터로 시각화하기 위해 GraphicLayer라는 클래스를 이용합니다. 그래픽 레이어는 다양한 그래픽 요소로 구성되는데, 새로운 종류의 그래픽 요소를 정의함(즉, 클래스를 확장함)으로써 공간 통계 데이터를 원하는 형태로 표현할 수 있습니다. 먼저 시각화하고자 하는 통계 데이터를 살펴보면 다음과 같습니다.

위의 통계 데이터를 그래픽 요소의 확장을 통해 시각화한 결과는 다음과 같습니다.

각 지역구 별로 코로나 확진자 수를 표현하고 있으며, 코로나 발생자가 많은 지역구은 빨간색으로, 적은 지역구는 초록색으로 표시하고 있습니다. 이제 위의 공간 통계 지도를 생성하기 위한 GIS 엔진의 API를 정리해 보겠습니다.

새로운 그래픽 요소를 추가하기 위해서는 GraphicRow를 부모 클래스로 하여 파생 클래스와 ShapeData를 부모 클래스로 하는 파생 클래스를 만들어 줍니다. GraphcRow의 파생 클래스는 통계 데이터가 어떻게 지도 상에 그려지는가를 정의하며, ShapeData의 파생 클래스는 그려지기 위해서 가져야할 데이터를 정의합니다. 앞서 본 통계 지도의 모습이 손톱 모양의 주제도라는 관점에서 각각의 파생 클래스를 NailNumberGraphicRow와 NailNumberShapeData라고 하겠습니다.

먼저 NailNumberGraphicRow 클래스의 코드에서 중요한 부분을 언급하면 다음과 같습니다.

NailNumberGraphicRow = Xr.Class({
    extend: Xr.data.GraphicRow,

    construct: function (id, /* NailNumberShapeData */ graphicData) {
        Xr.data.GraphicRow.call(this, id, graphicData.clone());

        // 그래픽 요소를 화면상에 시각화 하기 위해 필요한 심벌 정의
        // NailNumberGraphicRow 클래스에서는 PenSymbol 객체 2개, BrushSymbol 객체 2개, FontSymbol 객체 2개를 사용했음
    },

    methods: {
        MBR: function (/* CoordMapper */ coordMapper, /* SVG Element */ container) {
            // 그래픽 요소가 공간 상에 차지하는 MBR을 정의해서 반환
        },

        /* SVG Element */ appendSVG: function (/* CoordMapper */ coordMapper, /* SVG Element */ container) {
            // coordMapper는 지도 좌표를 화면 좌표로, 화면좌표를 지도 좌표로 변환하는 기능을 제공함
            // 표현되는 모습에 따라 SVG 자식 요소를 생성하여 SVG container에 추가 함
            // 자식 요소가 여러 개라면 g 요소를 부모로 하고, 이 g 요소를 반환함
        }
    }
});

GraphicRow의 파생 클래스는 최소한 MBR과 appendSVG 함수를 구현해야 합니다. 물론, 그래픽 요소의 편집을 위해서는 더 많은 함수와 인터페이스를 구현해야 하지만, 단순히 표현만을 위한다면 이 2개의 함수의 구현만으로도 충분합니다. 다음은 NailNumberShapeData 클래스의 코드입니다. 역시 중요한 부분만을 언급하면 다음과 같습니다.

Xr.data.NailNumberShapeData = Xr.Class({
    extend: Xr.data.ShapeData,

    construct: function (/* { pos: [x, y], 
                              outbox_size: [width, height], inbox_size: [width, height], 
                              title: '..', value: 0, title_offset_y: 0, value_offset_y: 0 } */ arg) {
        Xr.data.ShapeData.call(this);

        this._data = arg;
        this._mbr = new Xr.MBR();
    },

    methods: {
        /* ShapeData */ clone: function () {
            let arg = {};
            for (k in this._data) {
                arg[k] = this._data[k];
            }

            let newThing = new Xr.data.NailNumberShapeData(arg);
            newThing._mbr.copyFrom(this._mbr);

            return newThing;
        },

        data: function () {
            return this._data;
        },

        MBR: function () {
            return this._mbr;
        },

        /* PointD */ representativePoint: function () {
            // 그래픽 요소의 대표 좌표을 지정합니다. 
            // 대부분의 경우 MBR의 중심점이 대표 좌표입니다.
            return new Xr.PointD(this._mbr.centerX(), this._mbr.centerY());
        },

        /* int */ type: function () {
            return "NailNumberShapeData";
        },
    }
});

생성자에서 그래픽 요소로써 표현하는데 필요한 데이터들을 매개변수로 받습니다. 세부적인 API의 설명은 피하고 꼭 중요한 부분만을 언급하여 간단이 설명했지만, 공간 데이터의 시각화에 대해 원하는 어떠한 방법이라도 위의 방법을 통해 지원이 가능합니다.

FingerEyes-Xr에서 SHP 파일 자원을 외부 URL로 접근해 사용하기

예를 들어, http://www.gisdeveloper.co.kr:8080/SHP/seoul.zip와 같은 URL 경로로 접근할 수 있는 SHP 파일 자원을 웹 지도 컴포넌트인 FingerEyes-Xr에서 어떻게 가져와 지도를 구성하는 레이어로 사용하기 위한 코드를 정리합니다. zip 파일에는 반드시 .shp, .dbf, .shx 파일이 있어야 합니다.

let map = ...

let layerName = "레이어의 고유한 이름";
let URL = "http://www.gisdeveloper.co.kr:8080/SHP/seoul.zip";
let layer = new Xr.layers.SHPFileLayer(layerName, URL);

layer.EPSG(map.EPSG());
layer.proj4Name("EPSG:5179");

map.layers().add(layer, function () {
    map.updateLayer(layerName);
});

위의 코드에서 7번은 배경지도에 대한 좌표계의 EPSG 코드이며, 8번은 해당 shp 파일의 좌표계에 대한 EPSG 코드입니다.

아래의 글은 FingerEyes-Xr에서 사용자의 PC에 저장된 SHP 파일 자원 웹에서 활용하는 기능에 대한 설명입니다. URL을 통한 접근은 아니지면, 그 기반은 동일합니다.

NexGen, 웹 GIS에서 로컬 데이터 파일 활용

웹에서 SHP 파일 생성하기

SHP 파일은 최소 3가지 파일로 구성됩니다. 좌표 데이터가 저장된 .SHP, 이 좌표 데이터로 구성된 도형에 대한 인덱스가 저장된 .SHX, 속성 데이터가 저장된 .DBF 파일입니다.

웹 GIS에서 사용자가 공간 데이터를 활용하여 또 다른 의미의 공간 데이터를 생성해 낼 수 있을 것이고, 이 새로운 공간 데이터를 SHSP 파일 형태로 저장할 수 있다면 자신의 PC에 보관하거나, 다른 사용자와 파일 수준에서 공유할 수 있을 것입니다.

FingerEyes-Xr은 NexGen 솔루션 개발에 사용된 웹 GIS 클라이언트 라이브러리입니다. 이 FingerEyes-Xr에는 SHP 파일을 생성할 수 있는 기능을 제공하는데, 이에 대한 API를 정리해 둡니다.

먼저 생성하고자 하는 도형의 종류가 포인트인지, 폴리라인인지, 폴리곤인지를 지정하는 코드가 필요합니다. 여기서는 폴리곤입니다.

let shapeType = Xr.data.ShapeType.POLYGON;

속성 데이터의 구조를 정의하기 위해 아래의 코드가 필요합니다.

let fieldSet = new Xr.data.FieldSet();

fieldSet.add(new Xr.data.Field('field1', Xr.data.FieldType.STRING, 20));
fieldSet.add(new Xr.data.Field('field2', Xr.data.FieldType.INTEGER, 7));
fieldSet.add(new Xr.data.Field('field3', Xr.data.FieldType.FLOAT, 6, 2));

총 3개의 필드를 정의했으며 각각 문자열, 정수형, 실수형입니다. 문자열의 최대 길이는 20이며, 정수형의 최대 길이는 7이고, 실수형의 최대 길이는 6이면서 소수점 최대 길이는 2입니다.

이제 SHP 생성을 위한 팩토리를 정의합니다. 앞서 정의두었던 도형의 종류와 속성의 구조를 지정합니다.

let cntFields = fieldSet.count();
let factory = new Xr.export.ESRISHPFileFactory(fieldSet, shapeType);

이제 파일에 저장할 도형 좌표와 속성을 생성하고 팩토리에 추가합니다. 총 2개를 추가합니다.

let shape, attr;

shape = new Xr.data.PolygonShapeData([
    [
        new Xr.PointD(150267, 246895), new Xr.PointD(150367, 246895), new Xr.PointD(150367, 246995)
    ],
    [
        new Xr.PointD(150467, 247095), new Xr.PointD(150367, 247095), new Xr.PointD(150367, 247195)
    ]
]);

attr = new Xr.data.AttributeRow(-1, cntFields); // -1은 의미없음
attr.setValue(0, 'A가나다B');
attr.setValue(1, 100);
attr.setValue(2, 100.12);
let row1 = new Xr.export.RowSHP(shape, attr);

shape = new Xr.data.PolygonShapeData([
    [
        new Xr.PointD(150422, 246805), new Xr.PointD(150522, 246805), new Xr.PointD(150522, 246705)
    ]
]);

attr = new Xr.data.AttributeRow(-1, cntFields); // -1은 의미없음
attr.setValue(0, 'Hello');
attr.setValue(1, 200);
attr.setValue(2, 200.12);
let row2 = new Xr.export.RowSHP(shape, attr);

factory.addRow(row1);
factory.addRow(row2);

최종적으로 SHP 파일에 저장될 바이너리 데이터는 다음 코드를 통해 얻을 수 있습니다.

let shpObj = factory.export();

위의 shpObj 객체에는 shp, shx, dbf라는 속성이 존재하며 각각 앞서 언급한 .SHP, .SHX, .DBF 파일을 구성하는 바이너리 데이터가 ArrayBuffer의 배열로 담겨 있습니다. 실제 파일로의 저장은 아래의 코드를 통해 가능합니다. 속성 데이터에 대한 문자 인코딩은 UTF-8입니다.

if (isIE()) {
    saveToFile_IE('a.shp', shpObj.shp);
    saveToFile_IE('a.shx', shpObj.shx);
    saveToFile_IE('a.dbf', shpObj.dbf);
} else {
    saveToFile_Chrome('a.shp', shpObj.shp);
    saveToFile_Chrome('a.shx', shpObj.shx);
    saveToFile_Chrome('a.dbf', shpObj.dbf);
}

위의 코드에서 언급된 isIE, saveToFile_IE, saveToFile_Chrome 함수는 다음 글을 통해 상세히 파악할 수 있습니다. 참고로 코드 중 기존의 type: ‘text/plain’을 type: ‘application/zip’으로 변경했습니다.

웹에서 Javascript 만으로 텍스트 파일 생성

FingerEyes-Xr에서 문자열로 공간 데이터 추가하기

로컬 파일이나 URL Request를 통해, 또는 문자열 그대로.. 여튼, 아래와 같이 문자열로 구성된 데이터가 있다고 하자.

[
	{
		"주소":"전라남도 무안군 무안읍 면성1길 78",
		"인구":"100",
		"_상태":"OK",
		"WKT": "POINT(151985.4391669556 266232.22030393773)"
	},
	{
		"주소":"전라남도 무안군 무안읍 성남리 779-2",
		"인구":"50",
		"_상태":"OK",
		"WKT": "POINT(152027.07037272514 265628.6982788675)"
	},
	{
		"주소":"전라남도 무안군 무안읍 무안로 513-8",
		"인구":"77",
		"_상태":"OK",
		"WKT": "POINT(152432.06457469938 266037.0198316685)"
	}
]

위의 문자열에서 고려해야할 유일한 규칙은 좌표 데이터를 구성하기 위해서 WKT 필드가 활용(대소문자 구분)된다는 점이다. 그외의 필드는 모두 속성 필드로 해석된다. 이 JSON 문자열을 지도의 구성 단위인 레이어로 추가하기 위한 코드는 다음과 같다.

var json = 
    '[ \
        { \
            "주소": "전라남도 무안군 무안읍 면성1길 78", \
            "인구": "100", \
            "_상태": "OK", \
            "WKT": "POINT(151985.4391669556 266232.22030393773)" \
        }, \
        { \
            "주소": "전라남도 무안군 무안읍 성남리 779-2", \
            "인구": "50", \
            "_상태": "OK", \
            "WKT": "POINT(152027.07037272514 265628.6982788675)" \
        }, \
        { \
            "주소": "전라남도 무안군 무안읍 무안로 513-8", \
            "인구": "77", \
            "_상태": "OK", \
            "WKT": "POINT(152432.06457469938 266037.0198316685)" \
        } \
    ]';

var lyr = new Xr.layers.FeatureJSONLayer("레이어 이름", { EPSG: 4326, dataset: json });
map.layers().add(lyr);
map.update();

웹 GIS 클라이언트 엔진, FingerEyes-Xr

FingerEyes-Xr은 웹에서 지도를 표시하고, 표시된 지도와 연관된 기능을 제공할 수 있는 맵 컴포넌트입니다. GitHub에 소스가 공개되어 있으며, 다양한 GIS 시스템 개발을 위한 핵심 맵 컴포넌트로 활용되었고 웹 GIS 솔루션, NexGen에서도 지도 표출 및 다양한 지도 관련 기능을 위한 컴포넌트입니다.

FingerEyes-Xr은 완전한 웹 표준 기술만을 사용하였고, 모든 웹브라우저에서 구현된 표준 기술만을 사용하여 IE, Chrome, Safari, Firefox 등 거의 대부분의 브라우저에서 실행될 수 있습니다. FingerEyes-Xr은 이미 정해진, 구체적인 기능 개발을 지원하는 기반 컴포넌트인데, FingerEyes-Xr에서 지원하는 기능과 이 컴포넌트를 사용해 개발이 가능한 주요 기능은 다음과 같습니다.

먼저 핑거아이즈엑스알(FingerEyes-Xr)은 OGC 표준인 WFS, WMS 방식의 지도를 표시하여 활용할 수 있고, 항공영상을 TMS 방식 등을 통해 배경지도로 활용할 수 있습니다. 그리고 지도 위에 다양한 그래픽 요소를 매쉬업할 수 있는데, 표현할 수 있는 그래픽 요소에는 Marker, Polyline, Polygon, Box, Circle, Ellipse, Text, Image가 있고, 속성 데이터에 기반하여 복합적인 그래픽 요소를 공간 데이터의 차원으로 자유롭게 구성할 수 있는데, 이에 대한 자세한 내용은 아래 글을 참조하시기 바랍니다.

FingerEyes-Xr, 공간 데이터에 대한 Custom Draw (사용자 정의 그리기)

FingerEyes-Xr은 그래픽 요소와 공간 데이터를 웹에서도 자유롭게 편집할 수 있습니다. 이에 대한 구체적인 내용은 FingerEyes-Xr을 이용해 개발된 NexGen의 편집 기능을 통해 확인이 가능하며, 이와 관련된 글은 아래와 같습니다.

넥스젠(NexGen)의 공간 데이터 편집 기능

FingerEyes-Xr은 수치지도와 항공영상과 같은 이미지 지도 뿐만 아니라 라스터(Raster) 기반의 셀 데이터에 대한 연산이 가능하고 그 결과 지도 상에 표현할 수 있습니다. 관련된 내용은 아래와 같습니다.

[GIS] FingerEyes-Xr, 핑거아이즈로 생성한 밀집도(밀도도)

또한 통계 데이터를 주제도와 파이차트 등으로 표현하여 통계지도를 생성할 수 있는데, 이에 대한 간단한 예시는 다음과 같습니다.

FingerEyes-Xr에서 주제도(Theme Map) 표현

FingerEyes-Xr에서 파이 차트(Pie Chart) 표현하기

또한 FingerEyes-Xr은 사용자가 보다 쉽게 지도 기반의 기능을 활용할 수 있도록 다양한 지도 관련 UI를 제공하여, 개발자가 쉽게 지도 기반 UI를 구성할 수 있도록 지원합니다. 그리고 GIS 미들웨어 서버인 GeoService-Xr과 연계하여 지오코딩 관련 기능을 제공하며, 관련 내용은 다음과 같습니다.

넥스젠(NexGen)의 스타쿼리(* Query) 기능

그리고 표고값을 이용하여 표고측정, 지형의 표고값을 활용한 지형단면도, 지형 평균경사도, 지형 3차원 가시화 가능을 제공합니다. 이에 대한 내용으로 FingerEyes-Xr을 이용해 개발된 NexGen의 지형 관련 기능은 아래 글과 같습니다.

넥스젠(NexGen)의 DEM 데이터를 활용한 측정 기능

이외에도 FingerEyes-Xr를 이용하여 지도와 관련된 더 많은 기능을 지원하고 있고, 새로운 기능들이 추가되고 있습니다. FingerEyes-Xr을 통해 지도와 관련된 그 어떠한 기능이라도 기술의 제약 없이 개발할 수 있습니다.