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 ( ... ) {
        ...
    }
}

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

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의 설명은 피하고 꼭 중요한 부분만을 언급하여 간단이 설명했지만, 공간 데이터의 시각화에 대해 원하는 어떠한 방법이라도 위의 방법을 통해 지원이 가능합니다.