개요

공공개방되는 공간정보에는 건물도 포함됩니다. 건물 데이터는 수치지형도, 건물통합데이터, 도로명주소 건물 등 정보소스가 있습니다. 실제 데이터를 보게되면 건물 형상 등에 차이가 존재합니다. 필자는 단순히 형상이 틀립니다가 아닌 틀린 정도를 수치로 표현하려 합니다. 

본 게시글의 이해를 위하여 Hausdorff Distance에 대한 이해가 필요합니다. Hausdorff Distance는 점으로 이루어진 두 집합(포인트세트) 간의 거리를 결정하는 방법입니다. 두 집합 사이의 근접점에서 떨어진 가장 먼 지점을 찾습니다. 일반적으로 두 점(Point)간의 거리는 최단거리가 정의되는 거리입니다. 그러나 다각형의 경우 모든 점이 최단거리일 수 없습니다.

 

이를 적용하여 개별 공간 객체의 Hausdorff Distance를 구하여 객체 형상의 유사성을 검사할 수 있습니다. 거리 값이 작을수록 건물의 형상은 유사하다라고 정의할 수 있습니다. 

> 바로가기 : Hausdorff Distance (위키피디아)
> 바로가기 : Computing Geometric Similarity

 

본문

1. Hausdorff Distance의 이해

아래의 질의를 실행하여 객체의 좌표를 변경해가며 산출되는 거리를 보면 이해가 쉽습니다.
SELECT ST_HausdorffDistance('MULTIPOINT (0 0, 1 0)'::geometry, 'MULTIPOINT (0 2, 1 2, 2 2)'::geometry);

2. 검사 대상 데이터 준비

QGIS를 이용하여 건물통합 건물과 도로명주소 건물에서 여의도 지역만을 추출하여 4326좌표계로 전환한 후 PostGIS에 적재합니다. 실제 중첩한 형상을 보면 형상이 상당 부분 일치하지 않는 것을 볼 수 있습니다.

 

3. 거리 산출 수행 : ST_HausdorffDistance 함수 실행

수행 과정은 단순합니다. 그저 공간위상관계가 교차(ST_Intersects)하는 객체를 찾아 객체간 거리 산출(ST_HausdorffDistance) 함수를 적용하게 되면 두 건물간의 거리가 산출이 됩니다. 이를 통해 산출된 거리 값이 작을수록 두 건물은 유사한 형상을 가지고 있다라고 정의할 수 있습니다. 

SELECT DISTINCT ON(TEST_BD_1.bd_mgt_sn) TEST_BD_1.bd_mgt_sn, TEST_BD_1.bld_nm, TEST_BD_2.buld_nm, 
    CAST(ST_HausdorffDistance(TEST_BD_1.geom, TEST_BD_2.geom) as numeric) as hdist1
    FROM TEST_BD_1 INNER JOIN TEST_BD_2 ON ST_Intersects(TEST_BD_1.geom, TEST_BD_2.geom)
    ORDER BY TEST_BD_1.bd_mgt_sn, ST_HausdorffDistance(TEST_BD_1.geom, TEST_BD_2.geom);

 

맺음말
저는 단순히 거리만을 산출했습니다. 건물유형 별 산출된 거리의 평균값과 표준편차를 구한다면 전체 수준에서 데이터 유사성 수준을 평가할 수 있습니다. 필자가 전하고자 하는 메시지는 한결같습니다. 공간DBMS의 올바른 활용만으로 가성비좋은 앱을 제작할 수 있습니다.
즐 코딩 하십시오~~
 

 


개요

필자는 흔히 현장에서 GIS엔진이라고 말하는 미들웨어의 도입을 강력히 반대하는 입장입니다. 대부분의 2D GIS 사이트에서 GIS엔진은 성능을 떨어뜨리는 원인이기 때문입니다.
정보시스템은 정보저장 및 조회의 편의를 위하여 DBMS와 함께 동작합니다.(일부 예외는 존재합니다) 꽤 오래전부터 DBMS는 SDE(Spatial Data Extension)를 통해 공간데이터에 특화된 기능 및 함수를 제공합니다. SDE은 강력한 공간분석도구 입니다. 이의 활용을 통해 별도 미들웨어 없이 SQL만으로 Geo Processing을 수행할 수 있고 수행결과는 DBMS에 테이블 형식으로 정의함으로써 데이터 저장구조를 전체 시스템 수준에서 동일하게 가져갈 수 있는 장점이 있습니다.

공개SW인 PostGIS를 이용하여 경계분할을 진행합니다. 별도의 솔루션이 아닌 오직 DBMS가 지원하는 함수만을 이용합니다. 과정별 생성되는 데이터는 Qgis에서 확인했습니다.

본 글에서는 "보로노이다이어그램"을 이용하여 경계를 나누어 각각이 동일 면적에 근접하도록 분할하는 기법을 설명합니다. 이해를 위해 "보로노이 다이어그램"을 이해하셔야 합니다.

> 보로노이다이어그램 1 : 위키피디아
> 보로노이다이어그램 2 : 네이버캐스트

또한 K-Means 알고리즘에 대한 기반 지식 또한 요구됩니다.

> K-평균 알고리즘 : 위키피디아
> K-평균 군집화 : 티스토리(untitledblog)

이하 기술되는 SQL의 실행을 위하여 PostGIS의 GEOS 버전이 2.3.0 이상 이어야 합니다. 아래의 질의를 통해 PostGIS의 버전을 확인하고, 만일 기준을 충족하지 않을 경우 PostGIS를 업그레이드 하십시오. 간단한 설치 명령어만으로 쉽게 업그레이드 할 수 있습니다. 별도 설명하지 않겠습니다.

 select postgis_full_version();
POSTGIS="2.4.6 r17068" PGSQL="96" GEOS="3.7.0-CAPI-1.11.0 673b9939" SFCGAL="1.2.2" PROJ="Rel. 4.9.3, 15 August 2016" GDAL="GDAL 1.11.4, released 2016/01/25" LIBXML="2.9.1" LIBJSON="0.11" RASTER

 

본문

본 게시글에서 사용하는 PostGIS 함수는 다음과 같습니다. 상세 설명은 공식 사이트의 도큐먼트를 참조하십시오.(필독)

경계 분할 시행

1. 경계준비 : 시도경계 테이블에서 서울시 경계만을 추출한 별도 테이블 생성 (생략가능 과정)
CREATE TABLE seoul AS
    SELECT * FROM ngii_cdm_4326 WHERE bjcd = '1100000000';

2. 서울 경계 내에 존재하는 랜덤 포인트 셋 생성 : 포인트 개수가 많을 수록 최종 생성되는 분할 경계의 면적은 동일 값에 수렴
CREATE TABLE seoul_pts AS
    SELECT (ST_Dump(ST_GeneratePoints(geom, 10000))).geom AS geom FROM seoul WHERE bjcd = '1100000000';

3. K-Means 알고리즘을 이용한 군집화
CREATE TABLE seoul_pts_clustered AS
    SELECT ST_ClusterKMeans(geom, 10) over () AS cluster_id, geom FROM seoul_pts;

4. 클러스터 별 중심점 생성
CREATE TABLE seoul_pts_clustered_center AS
    SELECT cluster_id, ST_Centroid(ST_collect(geom)) AS geom FROM seoul_pts_clustered
    GROUP BY cluster_id;

5. 중심점을 기준으로 보로노이 다각형 생성
CREATE TABLE seoul_voronoi AS
    SELECT (ST_Dump(ST_VoronoiPolygons(ST_collect(geom)))).geom AS geom FROM seoul_pts_clustered_center;

6. 서울 경계와 보로노이 경계의 폐합 폴리곤 경계 생성
CREATE TABLE seoul_divided_result AS
    SELECT ST_Intersection(a.geom, b.geom) AS geom FROM seoul a
    CROSS JOIN seoul_voronoi b;

 

마치며

본 블로그의 카테고리 중 "꿀팁-PostGIS"가 상대적으로 빈약했습니다. 카테고리의 빈부격차를 줄이고자 임의의 시나리오를 만들어 게시글을 작성했습니다. 하지만 전하고자하는 메시지는 명확합니다.

공개SW가 보여주는 기능 및 성능은 필자를 많이 놀라게 합니다. 본 게시글에서 설명한 류의 Geo-Processing을 수행해야 한다면 저는 Shell Script를 만들어서 이를 호출하는 것으로 개발 코드를 많이 줄일 것 같습니다. 기능을 이행하는 수단은 한가지가 아니겠지만 가장 좋은 것은 이미 존재하여 검증된 것을 잘쓰는 것입니다. PostGIS는 이미 시장에서 검증되어 기술성숙도가 높은 공개SW입니다.

GIS엔진, 오라클이라는 굴레만 벗어난다면 가성비 좋은 아주 훌륭한 앱을 제작할 수 있습니다.

* 전체 소스코드 내용은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html lang='en'>
<meta charset="UTF-8">
<head>
<script src="js/Cesium-1.54/Build/Cesium/Cesium.js"></script>
<link href="js/Cesium-1.54/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<style>
    @import url(js/Cesium-1.54/Apps/Sandcastle/templates/bucket.css);
</style>
<div id="cesiumContainer" style="width:auto; height:700px"></div>
<script>
var extent = Cesium.Rectangle.fromDegrees(117.89628431.499028139.59738043.311528);
 
Cesium.Camera.DEFAULT_VIEW_RECTANGLE = extent;
Cesium.Camera.DEFAULT_VIEW_FACTOR = 0.7;
 
var viewer = new Cesium.Viewer('cesiumContainer',{
    timeline : false,
    animation : false,
    selectionIndicator : false,
    navigationHelpButton : false,
    infoBox : false,
    navigationInstructionsInitiallyVisible : false
});
 
var scene = viewer.scene;
// 위,경도 표출될 라벨의 옵션을 파라미터로 지정
var entity = viewer.entities.add({
    label : {
        show : true,
        showBackground : true,
        backgroundColor : Cesium.Color.BLACK,
        font : '25px sans-serif',
        horizontalOrigin : Cesium.HorizontalOrigin.LEFT,
        //verticalOrigin을 top으로 지정 
        verticalOrigin : Cesium.VerticalOrigin.TOP,
        //pixelOffset을 통해 label의 상세 위치를 지정 / Cartesian2 사용(x,y)
        pixelOffset : new Cesium.Cartesian2(150)
    }
});
// eventhandler 변수에 screenspacehandler를 담음
var eventhandler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
// movement가 발생하면 위,경도 값을 표출 
eventhandler.setInputAction(function(movement){
    var cartesian = viewer.camera.pickEllipsoid(movement.endPosition, scene.globe.ellipsoid);
    if(cartesian){
        var cartographic = Cesium.Cartographic.fromCartesian(cartesian);
        var longitude = Cesium.Math.toDegrees(cartographic.longitude).toFixed(2);
        var latitude = Cesium.Math.toDegrees(cartographic.latitude).toFixed(2);
        
        entity.position = cartesian;
        entity.label.show = true;
        entity.label.text = '경도 : '+('' + longitude).slice(-7+ '\u00B0' + '\n위도 : '+('' + latitude).slice(-7+ '\u00B0';
    }else{
        entity.label.show = false;
    }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
</script>
</head>
<body>
</body>
</html>
cs

 

* 28 ~ 40행은 위,경도를 표출할 라벨의 옵션을 key : value 값, 파라미터로 지정한 부분입니다.

 
var entity = viewer.entities.add({
    label : {
        show : true,
        showBackground : true,
        backgroundColor : Cesium.Color.BLACK,
        font : '25px sans-serif',
        horizontalOrigin : Cesium.HorizontalOrigin.LEFT,
        //verticalOrigin을 top으로 지정 
        verticalOrigin : Cesium.VerticalOrigin.TOP,
        //pixelOffset을 통해 label의 상세 위치를 지정 / Cartesian2 사용(x,y)
        pixelOffset : new Cesium.Cartesian2(150)
    }
});
cs

 

* VerticalOrigin에 대한 설명입니다. (자세한 내용은 CesiumDocument를 참고하세요.)

각 포지션을 통해 라벨이 표출되는 위치를 조정할 수 있습니다.

 

* Cartesian2에 대한 설명입니다. (자세한 내용은 CesiumDocument를 참고하세요.)

Cartesian2는 2차원적 개념으로 x, y 값을 갖게됩니다.

 

* 44 ~ 57행은 MouseMove 이벤트를 활용하여 movement가 발생함에 따라 위,경도 값이 표출되게끔 하는 부분입니다.

 
eventhandler.setInputAction(function(movement){
    var cartesian = viewer.camera.pickEllipsoid(movement.endPosition, scene.globe.ellipsoid);
    if(cartesian){
        var cartographic = Cesium.Cartographic.fromCartesian(cartesian);
        var longitude = Cesium.Math.toDegrees(cartographic.longitude).toFixed(2);
        var latitude = Cesium.Math.toDegrees(cartographic.latitude).toFixed(2);
        
        entity.position = cartesian;
        entity.label.show = true;
        entity.label.text = '경도 : '+('' + longitude).slice(-7+ '\u00B0' + '\n위도 : '+('' + latitude).slice(-7+ '\u00B0';
    }else{
        entity.label.show = false;
    }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
cs

cartographic변수에 cartesian을 활용한 위,경도 값을 담아줍니다.

 

* Cartographic에 대한 설명입니다. (자세한 내용은 CesiumDocument를 참고하세요.)

기본적으로 Cartesian3(x,y,z)값을 갖게되며 기본 투영체 및 좌표계로 WGS84를 사용한다는 것을 알 수 있습니다.

 

결과 화면 보겠습니다.

 

라벨이 마우스를 따라다니며 위,경도를 알려주는 것을 볼 수 있습니다. 제주 한라산의 위,경도는 사진과 같군요 

이번 시간에는 Cesium JS에서 gltf 포맷을 이용한 가상의 시나리오를 만들어 보겠습니다.

시나리오는 마포대교 위에 비행하는 드론이 실시간으로 교통상황을 알리는 내용입니다.

 

HeadingPitchRange 설명 (CesiumDocument)

HeadingPitchRange는 시각 및 관점에 대한 값을 정의하는것입니다.

Heading은 방향을 나타냅니다. 360'와 0' 그리고 프레임 중심을 기준으로 양의 값일때에는 동쪽, 음의 값일때에는 서쪽으로 각이 변경됩니다.

 

Pitch는 각을 나타냅니다. 여기서의 각은 평면(x,y)을 기준으로 양의 값일때에는 올라가고 음의 값일때에는 내려가게됩니다.

 

 

Range는 거리를 나타냅니다. 프레임 중심으로부터의 거리를 조절할 수 있습니다. 

 

상기 내용은 필자가 기본값(0.0)을 토대로 heading, pitch, roll 각각 임의의 값을 넣어서 테스트 한 것임을 알려드립니다.  

 

결과부터 보시겠습니다.

서비스바로가기

 

* 이번 게시물에서 필자가 중요하다고 생각하는 소스코드입니다. 

 
/*************
    heading: Heading변경 슬라이드 값 - 0~360까지의 값 
    pitch: Pitch변경 슬라이드 값- 0~150까지의 값
    range: Range변경 슬라이드 값 - 0~60까지의 값
    **************/    
    var staticPosition = Cesium.Cartesian3.fromDegrees(126.93320937.515165,2200);
    var orientation = new Cesium.HeadingPitchRange(heading, pitch, range);
    viewer.scene.camera.setView({
        // staticPosition -- (126.933209, 37.515165, 2200) <- 마포대교를 바라보기 위한 위치(좌표) 
        destination : staticPosition,    
        // orientation -- heading, pitch, range 값 
        orientation : orientation                
    });
cs

staticPosition이라는 변수에 마포대교를 바라보는 관점 즉, 위치를 지정합니다.

유저 컨트롤을 통해 획득한 값을 활용하여 관점을 변경할 수 있습니다.

 

* 결과화면을 보겠습니다. 마포대교 모델 상공에 드론 모델이 비행하는 모습을 볼 수 있습니다.

 

 

마포대교를 클릭 했을 시 차량 수와 교통 수준 정도가 조건을 토대로 랜덤하게 표출되는것을 볼 수 있습니다.

 

유저 컨트롤을 통해 heading, pitch, range 값을 조절할 수 있습니다. 슬라이드를 통해 각 값을 조절하고 변경 버튼을 통해 관점을 변경할 수 있습니다.

 

heading에 임의의 값을 주었더니 관점이 변경된 것을 볼 수 있습니다. 독자 여러분도 슬라이드와 버튼을 통해 관점을 변경하여 보세요.

Tip. 관점의 변경은 구체(원)를 생각하시면 이해하기 쉽습니다. 

openlayers3(이하 OL3)와 Leaflet Feature Click Event 이벤트를 알아보겠습니다.

서비스바로가기

Openlayers3 와 Leaflet 라이브러리를 받을 수 있는 곳은 아래와 같습니다. 

OpenLayers3 사이트  :  https://openlayers.org/ 

Leaflet 사이트 : http://leafletjs.com/

VWORLD 사이트  :  http://map.vworld.kr

 

1. Openlayers3(OL3)

<script></script>부분은 아래와 같습니다.

3행~15행 : wfs 데이터 불러오기

35행~ 46행 : OL3 지도 띄우기

48행~96행 : Feature Click 기능 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<script type="text/javascript">
    //wfs 불러오기
    var vectorSource = new ol.source.Vector({
        format : new ol.format.GeoJSON(),
        url: function(extent) {
            return 'http://*.***.**.***:8080/geoserver/progworks/wfs?service=WFS&' +
            'version=1.1.0&'+
            'request=GetFeature&'+
            'typename=progworks:z_upis_c_uq152&' +
            'outputFormat=application/json&'+
            'srsname=EPSG:3857&' +
            'bbox=' + extent.join(','+ ',EPSG:3857';
        },
        strategy: ol.loadingstrategy.bbox
    })
    
    //레스터 지도
    var raster = new ol.layer.Tile({
        source: new ol.source.XYZ({
               url: 'http://xdworld.vworld.kr:8080/2d/Base/201802/{z}/{x}/{y}.png'
           })
    });
 
    var vector = new ol.layer.Vector({
        source: vectorSource,
        style: new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: 'rgba(0, 0, 255, 1.0)',
                width: 2
            })
        })
    });
 
    //지도띄우기
    var map = new ol.Map({
        
        layers: [raster,vector],
       
        target: document.getElementById('map'),
       
        view: new ol.View({
          center: [14128579.824512570.74],
          maxZoom: 19,
          zoom: 14
        })
    });
    
    var select = null;
 
    var selectSingleClick = new ol.interaction.Select({
        multi: true
    });
 
    var selectClick = new ol.interaction.Select({
        condition: ol.events.condition.click,
        multi: true
    });
 
    var selectPointerMove = new ol.interaction.Select({
        condition: ol.events.condition.pointerMove,
        multi: true
    });
 
    var selectAltClick = new ol.interaction.Select({
        condition: function(mapBrowserEvent) {
            return ol.events.condition.click(mapBrowserEvent) &&
                ol.events.condition.altKeyOnly(mapBrowserEvent);
        },
        multi: true
    });
 
    var selectElement = document.getElementById('type');
 
    var changeInteraction = function() {
        if (select !== null) {
            map.removeInteraction(select);
        }
        var value = selectElement.value;
        if (value == 'singleclick') {
            select = selectSingleClick;
        } else if (value == 'click') {
            select = selectClick;
        } else if (value == 'pointermove') {
            select = selectPointerMove;
        } else if (value == 'altclick') {
            select = selectAltClick;
        } else {
            select = null;
        }
        if (select !== null) {
            map.addInteraction(select);
        }
    };
 
    selectElement.onchange = changeInteraction;
    changeInteraction();    
    
</script>
cs

 

<body></body>부분은 아래와 같습니다.

1
2
3
<body>
    <div id="map"></div>
</body>
cs

 

2. Leaflet

<script></script>부분은 아래와 같습니다.

4행 : leaflet 지도 띄우기

7행 : VWORLD 지도로 변경

9행 : wfs데이터 URL 설정

11행~19행 : 파라메타 설정

27행~51행 : wfs 로드 및 클릭이벤트 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<script type="text/javascript">
 
    //leaflet 지도 띄우기
    var leafletMap = L.map('leafletMap').setView([37.52470308242787126.9234],14)
    
    //Vworld Tile 변경
    L.tileLayer('http://xdworld.vworld.kr:8080/2d/Base/201802/{z}/{x}/{y}.png').addTo(leafletMap);
         
    var owsrootUrl = 'http://*.***.**.***:8080/geoserver/progworks/wfs';
    
    var defaultParameters = {
        service : 'WFS',
        version : '1.1.0',
        request : 'GetFeature',
        typeName : 'progworks:z_upis_c_uq152',
        outputFormat : 'application/json',
        format_options : 'callback:getJson',
        SrsName : 'EPSG:4326',
    };
    
    var parameters = L.Util.extend(defaultParameters);
    
    var URL = owsrootUrl + L.Util.getParamString(parameters);
    
    var WFSLayer = null;
    var selected;
    var ajax = $.ajax({
        url : URL,
        dataType : 'json',
        jsonpCallback : 'getJson',
        
        success: function(response) {
            WFSLayer = L.geoJson(response, {
                style: function (feature) {
                    return{
                        color: '#0000FF',
                        weight: 4
                    }; 
                }
            }).on('click'function (e) {
                if (selected) {
                  e.target.resetStyle(selected)
                }
                selected = e.layer
                selected.bringToFront()
                selected.setStyle({
                  'color''red'
                })
            }).addTo(leafletMap)
        }
    });
    
</script>
cs

<body></body>부분은 아래와 같습니다.

1
2
3
<body>
    <div id="leafletMap"></div>
</body>
cs

 

3. 결과화면

QGIS를 활용한 폴리곤 병합

이번 시간에는 Qgis를 활용하여 폴리곤을 병합하는 방법을 공유하도록 하겠습니다. 

필자는 Qgis 3.4 버전에서 작업하였음을 알립니다.

 

데이터 제공 - 센서스경계(시도경계)  :  

Z_SOP_BND_SIDO_PG.zip
다운로드

데이터 기본 좌표계 - UTM-K

데이터 출처 - 국가공간정보포털 (오픈마켓)

 

Qgis에 데이터를 업로드 합니다. (좌표계를 EPSG:5181로 지정해주세요.)

 

 

매우 중요한 부분입니다. 폴리곤 병합에 앞서 데이터의 "무결성 검증"을 실시해야 합니다. 무결성 검증의 순서는 다음과 같습니다.

  * 메뉴 -> 벡터 -> 도형 도구 -> 무결성 검증

* Tip - "무결성 검증" 을 실시하는 이유는 데이터가 병합 또는 여타 작업 실행 전, 오류 및 적합성을 판단하기 위함입니다. 

 

무결성 검증 실시 후 데이터 오류 처리하기

"무결성 검증"을 완료하게되면 유효한 산출물, 유효하지 않은 산출물, 오류 산출물 이렇게 3가지의 레이어가 생성됩니다.

 

오류를 처리하는 방법은 다음과 같습니다.

  * 오류 처리 할 레이어 선택 -> 편집 모드 켜기 -> 버텍스 툴 (VertextTool) 활성화 -> 오류 처리 

오류 지점을 자유롭게 선택 후 키보드 "delete"를 눌러 삭제합니다. 이와 같은 방법으로 다른 오류 지점도 처리하여 보세요.

오류 지점을 다 처리하였다면 편집 모드 토글을 재클릭하여 작업을 저장합니다.

 

데이터 오류 처리 후 레이어 병합하기

데이터 오류를 처리한 후 2가지 레이어 (유효한 산출물, 유효하지 않은 산출물)를 병합합니다. 순서는 다음과 같습니다.

실행 버튼을 눌러 병합을 진행합니다.

 

폴리곤 병합하기 

먼저, 데이터 오류 처리 작업까지 완료한 데이터를 업로드 합니다.

다시 한번 편집 모드를 켜준 후 "객체 선택" 토글을 누릅니다. 

 

객체 선택 기능을 활용하여 병합 할 폴리곤들을 다중 선택 합니다. (ctrl버튼과 좌클릭을 통해 다중선택을 쉽게 할 수 있습니다.)

 

선택한 객체 병합하기

선택한 폴리곤들을 병합하는 방법은 다음과 같습니다.

  * 메뉴 -> 편집 -> 선택한 객체 병합

 

확인을 눌러 병합을 완료합니다.

사진과 같이 두개의 폴리곤이 하나의 폴리곤으로 병합된 것을 볼 수 있습니다.

* Tip - 폴리곤을 병합할 시 병합할 수 없다는 에러 메세지가 표출되면 현재의 데이터로 재차 무결성 검증을 실시하세요.

  각 폴리곤 속성 테이블의 속성값이 달라도 병합이 가능합니다.  

Cesium JS Feature Picking 활용하기

이번 시간에는 CesiumJS에서 glTF 형식의 3D모델을 불러온 후 마우스 이벤트를 연동하겠습니다. 먼저 결과를 확인해보세요. (여의도로 가세요.)

서비스바로가기

 

* 전체 소스코드 내용은 다음과 같습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<!DOCTYPE html>
<html lang='en'>
<meta charset="UTF-8">
<head>
<script src="js/Cesium-1.54/Build/Cesium/Cesium.js"></script>
<link href="js/Cesium-1.54/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<style>
    @import url(js/Cesium-1.54/Apps/Sandcastle/templates/bucket.css);
</style>
<div id="cesiumContainer" style="width:auto; height:700px;"></div>
<script>
var extent = Cesium.Rectangle.fromDegrees(117.89628431.499028139.59738043.311528);
 
Cesium.Camera.DEFAULT_VIEW_RECTANGLE = extent;
Cesium.Camera.DEFAULT_VIEW_FACTOR = 0;
 
var viewer = new Cesium.Viewer('cesiumContainer', {
     timeline : false,
     animation : false,
     selectionIndicator : false,
     navigationHelpButton : false,
     infoBox : false,
     navigationInstructionsInitiallyVisible : false   
});
 
// Gltf포맷 활용가능하게함.
var scene = viewer.scene;
var position = Cesium.Cartesian3.fromDegrees(126.93451137.5210040);
var modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(position);
// fromGltf 함수를 사용하여 key : value 값으로 요소를 지정
var name = '프로그APT'
var model = viewer.scene.primitives.add(Cesium.Model.fromGltf({
    url : 'js/image/cesium.gltf',
    modelMatrix : modelMatrix,
    scale : 5000.0,
    name : name
}));
viewer.scene.primitives.add(model);
 
// nameoverlay를 위한 설정
var nameOverlay = document.createElement('div');
viewer.container.appendChild(nameOverlay);
nameOverlay.className = 'backdrop';
nameOverlay.style.display = 'none';
nameOverlay.style.position = 'absolute';
nameOverlay.style.bottom = '0';
nameOverlay.style.left = '0';
nameOverlay.style['pointer-events'= 'none';
nameOverlay.style.padding = '4px';
nameOverlay.style.backgroundColor = 'black';
 
// feature select 
var selected = {
    feature : undefined,
    originalColor : new Cesium.Color()
};
 
// 모델을 select하기위한 부분
var selectedEntity = new Cesium.Entity(model);
 
// 클릭핸들러 변수선언
var clickHandler = viewer.screenSpaceEventHandler.getInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
 
if(Cesium.PostProcessStageLibrary.isSilhouetteSupported(viewer.scene)){
    // 블루, 그린 실루엣 지원
    var silhouetteBlue = Cesium.PostProcessStageLibrary.createEdgeDetectionStage();
    silhouetteBlue.uniforms.color = Cesium.Color.BLUE;
    silhouetteBlue.uniforms.length = 0.01;
    silhouetteBlue.selected = [];
    
    var silhouetteGreen = Cesium.PostProcessStageLibrary.createEdgeDetectionStage();
    silhouetteGreen.uniforms.color = Cesium.Color.LIME;
    silhouetteGreen.uniforms.length = 0.01;
    silhouetteGreen.selected = [];
    
    viewer.scene.postProcessStages.add(Cesium.PostProcessStageLibrary.createSilhouetteStage([silhouetteBlue, silhouetteGreen]));
    // mousemove function을 이용하여 모델을 select 후 배열에 담음
    viewer.screenSpaceEventHandler.setInputAction(function onMouseMove(movement){
        silhouetteBlue.selected = [];
    // 모델에 마우스가 move될 시 nameoverlay 출현    
    var pickedFeature = viewer.scene.pick(movement.endPosition);
    if(!Cesium.defined(pickedFeature)){
        nameOverlay.style.display = 'none';
        return;
    }
    // nameOverlay 설정
    nameOverlay.style.display = 'block';
    nameOverlay.style.bottom = viewer.canvas.clientHeight - movement.endPosition.y + 'px';
    nameOverlay.style.left = movement.endPosition.x +'px';
    
    nameOverlay.textContent = name;
    
    if(pickedFeature !== selected.feature){
        silhouetteBlue.selected = [model];
    }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
    
    // 모델을 클릭하여 알럿창을 띄우기위한 부분 / function onLeftClick
    viewer.screenSpaceEventHandler.setInputAction(function onLeftClick(movement){
        // silhouetteGreen 변수에 담을것
        silhouetteGreen.selected = [];
        
        // 새로운 모델 선택
        var pickedFeature = viewer.scene.pick(movement.position);
        if(!Cesium.defined(model)){
            clickHandler(movement);
            return;
        }
        
        // 선택된 모델이 없으면 새로운 모델 선택 가능
         if(silhouetteGreen.selected[model] === pickedFeature){
             return;
        } 
         alert("이곳은" +name+ "입니다." +'\n'+ "높이 : "+"68.7"+"m" +'\n'+ "넓이 : "+"30.2"+"m" +'\n'+ "가구 수 : " +"1000"+ "세대");
         
        // 샌택된 모델의 기존색상 저장(파란색)
        var highlightedFeature = silhouetteBlue.selected[model];
        if(model === highlightedFeature){
            silhouetteBlue.selected = [];
        }
        
        // 새로 선택된 모델 highlight
        silhouetteGreen.selected = [model];
        
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
    
}
</script>
</head>
<body>
</body>
</html>
cs

 

40 ~ 50 행 : 모델로 마우스가 이동했을 시 모델의 이름을 표출할 nameOverlay에 대한 설정입니다.

 
// nameoverlay를 위한 설정
var nameOverlay = document.createElement('div');
viewer.container.appendChild(nameOverlay);
nameOverlay.className = 'backdrop';
nameOverlay.style.display = 'none';
nameOverlay.style.position = 'absolute';
nameOverlay.style.bottom = '0';
nameOverlay.style.left = '0';
nameOverlay.style['pointer-events'= 'none';
nameOverlay.style.padding = '4px';
nameOverlay.style.backgroundColor = 'black';
cs

 

65 ~ 96행 : 블루, 그린 실루엣을 사용하기위한 변수를 선언하고 onMouseMove function을 활용하기위한 부분입니다. 

 
// 블루, 그린 실루엣 지원
    var silhouetteBlue = Cesium.PostProcessStageLibrary.createEdgeDetectionStage();
    silhouetteBlue.uniforms.color = Cesium.Color.BLUE;
    silhouetteBlue.uniforms.length = 0.01;
    silhouetteBlue.selected = [];
    
    var silhouetteGreen = Cesium.PostProcessStageLibrary.createEdgeDetectionStage();
    silhouetteGreen.uniforms.color = Cesium.Color.LIME;
    silhouetteGreen.uniforms.length = 0.01;
    silhouetteGreen.selected = [];
    
    viewer.scene.postProcessStages.add(Cesium.PostProcessStageLibrary.createSilhouetteStage([silhouetteBlue, silhouetteGreen]));
    // mousemove function을 이용하여 모델을 select 후 배열에 담음
    viewer.screenSpaceEventHandler.setInputAction(function onMouseMove(movement){
        silhouetteBlue.selected = [];
    // 모델에 마우스가 move될 시 nameoverlay 출현    
    var pickedFeature = viewer.scene.pick(movement.endPosition);
    if(!Cesium.defined(pickedFeature)){
        nameOverlay.style.display = 'none';
        return;
    }
    // nameOverlay 설정
    nameOverlay.style.display = 'block';
    nameOverlay.style.bottom = viewer.canvas.clientHeight - movement.endPosition.y + 'px';
    nameOverlay.style.left = movement.endPosition.x +'px';
    
    nameOverlay.textContent = name;
    
    if(pickedFeature !== selected.feature){
        silhouetteBlue.selected = [model];
    }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
cs

* MouseMove 이벤트에 대한 설명 (Cesium Document를 통해 자세한 내용을 살펴보세요.)

마우스의 움직임으로 이벤트를 구현할 수 있습니다.

 

98 ~ 125행 : onLeftClick 함수를 사용하여 새로운 모델 선택 및 모델 실루엣 색상을 변경하기위한 부분입니다. 

 
// 모델을 클릭하여 알럿창을 띄우기위한 부분 / function onLeftClick
    viewer.screenSpaceEventHandler.setInputAction(function onLeftClick(movement){
        // silhouetteGreen 변수에 담을것
        silhouetteGreen.selected = [];
        
        // 새로운 모델 선택
        var pickedFeature = viewer.scene.pick(movement.position);
        if(!Cesium.defined(model)){
            clickHandler(movement);
            return;
        }
        
        // 선택된 모델이 없으면 새로운 모델 선택 가능
         if(silhouetteGreen.selected[model] === pickedFeature){
             return;
        } 
         alert("이곳은" +name+ "입니다." +'\n'+ "높이 : "+"68.7"+"m" +'\n'+ "넓이 : "+"30.2"+"m" +'\n'+ "가구 수 : " +"1000"+ "세대");
         
        // 샌택된 모델의 기존색상 저장(파란색)
        var highlightedFeature = silhouetteBlue.selected[model];
        if(model === highlightedFeature){
            silhouetteBlue.selected = [];
        }
        
        // 새로 선택된 모델 highlight
        silhouetteGreen.selected = [model];
        
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
cs

* LeftClick 이벤트에 대한 설명 (Cesium Document를 통해 자세한 내용을 살펴보세요.)

마우스 좌클릭을 통해 선택, 드래그 등의 이벤트를 구현할 수 있습니다.

 

결과 화면을 보겠습니다. 

마우스를 모델에 올렸을 시 모델의 테두리 색상이 파란색으로 변경되고 nameoverlay가 활성화 된 것을 볼 수 있습니다.

 

 

모델을 클릭했을 시 Alert창에 모델의 이름과 임의로 지정한 값을 알립니다.

 

모델을 클릭 후 테두리 색상에 변경된것을 확인할 수 있습니다. 선택했던 모델에 재차 마우스를 올려도 nameOverlay가 활성화됩니다.

 

웹 소켓(Web Socket) 이란

웹소켓(WebSocket)은 하나의 TCP 접속에 전이중 통신 채널을 제공하는 컴퓨터 통신 프로토콜이다. 웹소켓 프로토콜은 2011년 IETF에 의해 RFC 6455로 표준화되었으며 웹 IDL의 웹소켓 API는 W3C에 의해 표준화되고 있다. (위키피디아)

더 쉽게 말하면 웹 소켓은 웹 서버와 웹 브라우저가 연결된 네트워크(TCP)를 통해 실시간으로 데이터를 주고 받는 기술이며 HTML5에 정의된 사양이다. 일반적으로 웹소켓은 일반 HTML 의 요청/응답과 비교하여 약 50배 정도의 성능 향상이 있는 것으로 보고되고 있다.

[출처] https://github.com/jwvg0425/ProjectArthas/wiki/Network-(TCP-IP-기본)

목적 및 작업 계획

동일한 지도 화면을 공유하여 작업상황을 모니터링 하고, 상황에 따른 지휘 통제를 위해 동일한 작업을 그룹 내에서 공유

일반적인 화상회의 솔루션이 현재 화면의 메모리덤프 형식으로 동작하여 래스터방식의 현재 화면 상태 만을 전송함. 이에 지도 위에서 일어나는 편집 (점, 선, 면) 작업의 저장 및 활용에 제약이 존재하여 GIS 전자지도가 제공하는 기능의 활용에 애로가 있음

  1. 이에 대한 해결 방안으로써 웹 소켓을 이용하여 작업자가 지도 위에서 생성한 객체 정보를 그룹 내 다른 사용자에게 전달
  2. Sender가 보낸 Message를 수신한 사용자 브라우저의 지도 위에 시각화
  3. 앞의 과정을 통해 그룹 내 모든 사용자는 동일한 GIS 실행 화면을 공유


실행 환경 설정

1. Server : Linux Container, 가상서버 위에 설치되는 DBMS
2. WAS(Web Application Server) : Apache Tomcat

 

구성 요약

1. Socket 서버 : 웹 브라우저와 메시지 교환

      • Socket의 기본형태는 위 소스와 같이 Open, Close, Error, Message로 구분된다. 
      • 51행 : @onMessage를 통해 GIS Sender로부터 받은 좌표정보를 Receiver 페이지로 전달
      • 61행 : Recevier 페이지로 전달
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package egovframework.map.service;
 
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
 
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
 
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;
 
import egovframework.map.web.Coordinate;
 
@Controller
@ServerEndpoint("/websocket")
public class WSocketServer {
 
    static List<Session> sessionUsers = Collections.synchronizedList(new ArrayList<Session>());
    
 
    @OnOpen
    public void onOpen(Session userSession) {
        System.out.println("Open Connection!...");
        sessionUsers.add(userSession);
    }
 
    @OnClose
    public void onClose(Session userSession) {
        System.out.println("Close Connection!...");
        sessionUsers.remove(userSession);
    }
 
    @OnError
    public void onError(Throwable e) {
        e.printStackTrace();
    }
    
    @OnMessage
    public void onMessage(String message, Session userSession) throws IOException {
        System.out.println(message);
        
        Iterator<Session> itr = sessionUsers.iterator();
        
        while (itr.hasNext()) {
            try {
                Session session = itr.next();
                
                session.getBasicRemote().sendText(message);
                
            } catch (IOException e) {                        
                e.printStackTrace();
            }
        }
        
    }
 
}
 
cs

 

2. GIS Sender

GIS Sender는 화면공유를 할 수 있는 관리자 페이지로 지도상에서 선택한 위치정보를 포인트 형태로 공유할 수 있게 정보를 전송하는 페이지다. GIS Sender 페이지의 소스는 아래와 같다.

      • GIS Sender의 <scrip></script>부분은 GIS Sender 페이지 로딩 부분과 WebSocket 통신을 통해 정보를 공유하는 부분 이렇게 2개로 구분된다. 
      • 32행 ~ 106행 <scrip></script> : 지도 로딩 및 클릭 이벤트  
      • 124행 ~ 155행 <scrip></script> :  WebSocket 서버 통신 및 Message 등 설정
      • 126행 : WebSocket 서버 접속

141행 : WebSocket으로 GeoJson형태의 위치정보 전송 및 Textarea에 좌표정보 표출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ page session="false" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Tomcat WebCommander</title>
<link type="text/css" rel="stylesheet" href="<c:url value='/css/openlayers/ol.css'/>" />
<link type="text/css" rel="stylesheet" href="<c:url value='/css/bootstrap/bootstrap.min.css'/>" />
 
<script src="<c:url value='/js/openlayers/ol.js'/>"></script>
<script src="<c:url value='/js/jquery/jquery-1.12.4.min.js'/>"></script>
<script src="<c:url value='/js/bootstrap/bootstrap.min.js'/>"></script>
 
<style>
#map {
    height: 550px;
    width: 100%;
}
 
#echoText{
    height: 200px;
    width: 100%;
}
#message{
    width: 300px;
    margin: 10px;
}
</style>
 
</head>
<script type="text/javascript">
var map;
var hdms;
var geojsonStr;
var iconStyle = new ol.style.Style({
    image: new ol.style.Icon(({
        anchor: [0.580],
        anchorXUnits: 'fraction',
        anchorYUnits: 'pixels',
        src: '/images/common/logo.png',
    size: [150150],
    scale: 0.5
    }))
});
    
var writer = new ol.format.GeoJSON();
 
var vectorSource = new ol.source.Vector();
 
var vectorLayer = new ol.layer.Vector({
    source: vectorSource
});
 
$(document).ready(function(){
    
    var extent = [0.82683994988722991.296765637000096168.1705899498872449.63660938700009];
    
    map = new ol.Map({
        layers: [
              new ol.layer.Tile({
                source: new ol.source.OSM({
                    
                })
              }),
              vectorLayer
        ],
        target: 'map',
        interactions: ol.interaction.defaults({
            dragPan: false
        }),
        view: new ol.View({
            projection : 'EPSG:4326',     
            center: ol.extent.getCenter(extent),
              zoom: 3,
              maxZoom: 3,
              minZoom:3
        })
    });
    
    map.on('click'function(evt) {
 
        vectorSource.clear();
        
        var coordinate = evt.coordinate;
        hdms = ol.coordinate.toStringHDMS(coordinate);
        
        var coordLon = coordinate[0];
        var coordLat = coordinate[1];
        
        var iconFeature = new ol.Feature({
            geometry: new ol.geom.Point([coordLon,coordLat])
          });
        
        iconFeature.setStyle(iconStyle);
        
        vectorSource.addFeature(iconFeature);
    
        $('#message').attr('value',coordinate);        
        
        geojsonStr = writer.writeFeatures(vectorSource.getFeatures());
        
    });
 
});
</script>
<body>
    <nav class="navbar navbar-dark bg-primary">
        <div class="container-fluid">
            <div class="navbar-header">
                <h1>GIS Sender </h1>
            </div>
        </div>
    </nav>
    <h1>Web Socket을 활용한 GIS 화면 공유 </h1>
    <form>
        <input id="message" type="text">
        <input onclick="wsSendMessage();" value="위치 전송" type="button">
        <input onclick="wsCloseConnection();" value="Disconnect" type="button">
    </form>
    <div id="map"></div>
    <br>
    <textarea id="echoText" rows="500" cols="700"></textarea>
    <script type="text/javascript">
        //웹소켓 서버 접속
        var webSocket = new WebSocket("ws://mapview.paas.lx.or.kr/websocket");
        
        var echoText = document.getElementById("echoText");
        echoText.value = "";
        
        var message = document.getElementById("message");
        
        webSocket.onopen = function(message){ wsOpen(message);};
        webSocket.onclose = function(message){ wsClose(message);};
        webSocket.onerror = function(message){ wsError(message);};
        
        
        function wsOpen(message){
            echoText.value += "Connected ... \n";
        }
        function wsSendMessage(){
            webSocket.send(geojsonStr);
              echoText.value += "GIS sender dent the location information to the Recevier : " + hdms + "\n";
        }
        function wsCloseConnection(){
            webSocket.close();
        }
        function wsClose(message){
            echoText.value += "Disconnect ... \n";
        }
 
        function wserror(message){
            echoText.value += "Error ... \n";
        }
    </script>
    <footer class="page-footer font-small blue">
 
          <div class="footer-copyright text-center py-3">
            
            <a href="https://progworks.tistory.com">
                <img alt="logo" width="70px" height="70px" src="/images/common/logo.png">
            </a>
            PROGWORKS - https://progworks.tistory.com/
            
          </div>
    </footer>
</body>
</html>
 
cs

3. Receiver

Receiver는 GIS Sender를 통해 보내진 위치정보를 사용자가 공유 받는 페이지이다.

      • Receiver 페이지는 GIS Sender 페이지와 유사하지만 별도의 기능이 없이 WebSocket을 통해 Sender페이지에서 받은 위처정보만을 지도상에 보여준다.
      • Receiver 페이지 또한 지도 로딩과 WebSocket 통신부분으로 2개의 <scrip></script>부분이 존재한다.
      • 28행 ~ 76행 <script></script> : 지도 로딩
      • 90행 ~ 137행 <script></script> : WebSocket 서버 통신, message 및 icon 생성
      • 92행 : WebSocket 접속
      • 110행 : WebSocket을 통해 위치정보를 전달받아 function addFeature() 실행 및 Textarea에 좌표정보 표출
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ page session="false" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Tomcat WebSocket</title>
<link type="text/css" rel="stylesheet" href="<c:url value='/css/openlayers/ol.css'/>" />
<link type="text/css" rel="stylesheet" href="<c:url value='/css/bootstrap/bootstrap.min.css'/>" />
 
<script src="<c:url value='/js/openlayers/ol.js'/>"></script>
<script src="<c:url value='/js/jquery/jquery-1.12.4.min.js'/>"></script>
<script src="<c:url value='/js/bootstrap/bootstrap.min.js'/>"></script>
 
<style>
#map {
    height: 550px;
    width: 100%;
}
 
#echoText{
    height: 200px;
    width: 100%;
}
</style>
 
</head>
<script type="text/javascript">
var map;
var hdms;
 
var iconStyle = new ol.style.Style({
    image: new ol.style.Icon(({
        anchor: [0.580],
        anchorXUnits: 'fraction',
        anchorYUnits: 'pixels',
        src: '/images/common/logo.png',
    size: [150150],
    scale: 0.5
    }))
});
var vectorSource = new ol.source.Vector();
 
var vectorLayer = new ol.layer.Vector({
    source: vectorSource
});
 
$(document).ready(function(){
    var extent = [0.82683994988722991.296765637000096168.1705899498872449.63660938700009];
    
    var proj = new ol.proj.Projection({
        code: 'EPSG:4326',
        units: 'm',
        extent: extent
    });
    
    map = new ol.Map({
        layers: [
              new ol.layer.Tile({
                source: new ol.source.OSM()
              }),
              vectorLayer
        ],
        target: 'map',
        interactions: ol.interaction.defaults({
            dragPan: false
        }),
        view: new ol.View({
            projection : 'EPSG:4326',     
            center: ol.extent.getCenter(extent),
            zoom: 3,
              maxZoom: 3,
              minZoom:3
        })
    });
});
</script>
<body>
    <nav class="navbar navbar-dark bg-primary">
        <div class="container-fluid">
            <div class="navbar-header">
                <h1>Receiver </h1>
            </div>
        </div>
    </nav>
    <h1>Web Socket을 활용한 GIS 화면 공유 </h1>    
    <div id="map"></div>
    <br>
    <textarea id="echoText" rows="500" cols="700"></textarea>
    <script type="text/javascript">
          //웹소켓 서버 접속        
        var webSocket = new WebSocket("ws://mapview.paas.lx.or.kr/websocket");
          
        var echoText = document.getElementById("echoText");
        echoText.value = "";
        
        var message = document.getElementById("message");
        
        webSocket.onopen = function(message){ wsOpen(message);};
        webSocket.onmessage = function(message){ wsGetMessage(message);};
        webSocket.onclose = function(message){ wsClose(message);};
        webSocket.onerror = function(message){ wsError(message);};
        
        function wsOpen(message){
            echoText.value += "Connected ... \n";
        }
        function wsCloseConnection(){
            webSocket.close();
        }
        function wsGetMessage(message){
            addFeature(message.data);
              echoText.value += "Location information received from to the GIS Sender : " + hdms + "\n";
        }
        function wsClose(message){
            echoText.value += "Disconnect ... \n";
        }
 
        function wserror(message){
            echoText.value += "Error ... \n";
        }
        
        function addFeature(ft){
            
            vectorSource.clear();
            
            var selectFt = ft;
            var writer = new ol.format.GeoJSON();
            var features = writer.readFeatures(selectFt);
            
              for (var i = 0; i < features.length; i++) {
                  features[i].setStyle(iconStyle)
                  coordInfo = features[i].getGeometry().getCoordinates()
                  hdms = ol.coordinate.toStringHDMS(coordInfo);
                  vectorSource.addFeature(features[i]);
            }
        }
    </script>
    <footer class="page-footer font-small blue">
 
          <div class="footer-copyright text-center py-3">
            
            <a href="https://progworks.tistory.com">
                <img alt="logo" width="70px" height="70px" src="/images/common/logo.png">
            </a>
            PROGWORKS - https://progworks.tistory.com/
            
          </div>
    </footer>
</body>
</html>
 
cs

 

실행 및 사용법

실행을 시키기 위해 GIS Sender 페이지와 Receiver 페이지에 접속한다. 테스트를 진행하기 위해 Receiver 페이지는 크롬, Microsoft Edge, IE 3곳에서 실행한다.
(Sender 한 명이 지도 상에 위치를 선택하면 선택한 위치가 사이트에 접속되어 있는 모든 사용자에게 동일하게 나타남)

    1. GIS Sender 및 Receiver 페이지(크롬, Microsoft Edge, IE)에 접속을 한다. 
    2. GIS Sender 페이지에서 지도상에 임의의 지점을 클릭한다.
    3. 좌측 상단 <input id="message" type="text">부분에 좌표정보가 출력되는지 확인한다. 
    4. 좌표 전송 버튼을 클릭한다. <textarea id="echoText" rows="500" cols="700"></textarea>부분에 아래와 같은 문구와 함께 좌표정보가 나타나면 정상작동된것이다.
      ex) "GIS sender sent the location information to the Recevier : 48° 03′ 21″ N 69° 06′ 29″ E"
    5. Receiver 페이지에서 좌표정보를 받아 지도상에 마커 및 <textarea id="echoText" rows="500" cols="700"></textarea>부분에 좌표정보가 출력되는지 확인한다.
      ex) "Location information received from to the GIS Sender : 48° 03′ 21″ N 69° 06′ 29″ E"

 

 

적용 및 응용 분야

  1. 실시간 상황 공유 기반 지휘 관제
  2. 폴리곤, 선 등 동일한 객체를 다중 편집자가 동시 작업
  3. 화면 덤프 방식으로 공유하는 영상회의 솔루션과 함께 사용되어 GIS 벡터 형식 자료 공유

 

결론

1. HTML5 웹 소켓을 통해 GIS 기반 위치 Push, 동일 객체 다중 편집 등이 가능함을 확인
2. 보통 채팅 등의 용도로 이용되는 웹 소켓이 GIS 부문 응용 앱의 기능을 더욱 풍부하게 할 수 있음을 확인

이번 시간에는 3D Builder를 통한 GLTF포맷 데이터 생성 및 이를 Cesium JS에 활용하는 방법을 공유하겠습니다.
먼저 결과를 확인해보세요. (여의도로 가세요.)

서비스바로가기

 

* glTF 포맷이란?

JSON 표준을 사용하는 3D장면 및 모델의 파일 형식이며 3D장면의 크기를 축소하고 런타임 처리를 최소화하는 

효율적이고 상호 운용 가능한 자산 전달 형식입니다. 

> 바로가기 : glTF Overview (https://www.khronos.org/gltf/)

 

* 3D Builder 사용하기

1. Window '검색'을 통해 3D Builder를 검색하세요. 

Tip. Window에 3D Builder가 기본 제공 되어있지 않은 경우 "MicroSoft Stroe"를 통해 다운로드 받으시길 바랍니다.

2. 3D Builder를 실행한 후 새 장면을 클릭합니다.

Tip. 각 기능에 대한 설명은 "3D Builder 사용자 가이드"를 참고하세요.

3. 3D 모델 생성 및 텍스쳐 입히기

우선, 정육면체 1개를 추가해봅시다. 

 

 

그리고 그리기 메뉴에서 텍스쳐 탭을 클릭 후 임의의 텍스쳐를 입혀봅니다.

 

 

저장을 합니다.

Tip. 저장시에는 파일형식을 "glTF format"형식으로 해야합니다. 

사진과 같이 gltf, bin 형식의 파일 그리고 설정한 텍스쳐 파일이 저장되었습니다.

 

필자는 아래 사진과 같은 3D Model을 생성하였습니다.

 

 

* 소스코드 작성하기 

glTF 포맷을 사용하기 위한 소스코드

 
// Gltf포맷 사용을 위한 변수들 선언
var scene = viewer.scene;
var position = Cesium.Cartesian3.fromDegrees(126.93451137.5210040);
var modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(position);
 
// fromGltf 함수를 사용하여 key : value 값으로 요소를 지정
scene.primitives.add(Cesium.Model.fromGltf({
    url : 'js/image/cesium.gltf'// gltf포맷의 위치 
    modelMatrix : modelMatrix,
    scale : 5000.0
}));
cs

 

전체 소스코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang='en'>
<meta charset="UTF-8">
<head>
<script src="js/Cesium-1.54/Build/Cesium/Cesium.js"></script>
<link href="js/Cesium-1.54/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<title>Make Building</title>
<div id="cesiumContainer" style="width:auto; height:925px;"></div>
<script>
var extent = Cesium.Rectangle.fromDegrees(117.89628431.499028139.59738043.311528);
 
Cesium.Camera.DEFAULT_VIEW_RECTANGLE = extent;
Cesium.Camera.DEFAULT_VIEW_FACTOR = 0;
 
var viewer = new Cesium.Viewer('cesiumContainer', {
     timeline : false,
     animation : false,
     selectionIndicator : false,
     navigationHelpButton : false,
     infoBox : false,
     navigationInstructionsInitiallyVisible : false   
});
 
// Gltf포맷 사용을 위한 변수들 선언
var scene = viewer.scene;
var position = Cesium.Cartesian3.fromDegrees(126.93451137.5210040);
var modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(position);
// fromGltf 함수를 사용하여 key : value 값으로 요소를 지정
scene.primitives.add(Cesium.Model.fromGltf({
    url : 'js/image/cesium.gltf'// Gltf포맷의 위치
    modelMatrix : modelMatrix,
    scale : 5000.0
}));
</script>
</head>
<body>
</body>
</html>
cs

 

결과 화면은 다음과 같습니다. 

필자는 여의도 지역의 일부를 생성하였습니다.

SVG(Scalable Vector Graphics) 정의

> 바로가기 : W3C SVG Specfication (https://www.w3.org/TR/SVG2/)
> 바로가기 : W3School SVG Tutorial (https://www.w3schools.com/graphics/svg_intro.asp)
> 바로가기 : SVG 애니메이션 레벨2 (https://svgwg.org/specs/animations/)

동적 웹페이지를 위하여 플래쉬가 널리 사용되던 적이 있습니다. 현재도 사용되긴 하지만 웹 호환성 문제가 나타나며 점진적으로 사용이 감소하고 있는 추세입니다. 플래쉬를 충분히 대체할 수 있는 웹 표준 기술이 존재하기 때문에 플래쉬는 결국 사장될 것입니다. 이를 대체하는 대표적인 표준 기술이 SVG 입니다.

SVG(Scalable Vector Graphics)는 XML 문서 형식으로 2차원 벡터 그래픽 객체를 정의하는 웹 표준으로 1999년 W3C(World Wide Web Consortium)의 주도로 개발되었습니다. 당시 인터넷 이용 환경은 차별없는 웹 구현에 제약이 있었습니다. 웹 브라우저에서 그래픽 객체를 표현하기 위해 Flash, Applet, ActiveX 등 벤더 종속적인 Binary Plug-in 을 사용했으나 상호운영성 및 호환성에 부족한 부분이 있었습니다.

현재 신규 제작되는 웹사이트의 경우 대부분 HTML5 사양이 적용되고 있습니다. HTML5가 이전 HTML 사양과 구별되는 기술직인 특징은 멀티미디어(video, audio) 사용성 강화, 그래픽 표현 강화(svg, canvas), Socket 적용 등을 들 수 있습니다. HTML5가 정의하는 2차원 벡터 그래픽 표현 방법은 SVG를 사용합니다. 이를 통해 HTML5는 SVG가 가지는 장점을 수용합니다. SVG가 가지는 장점은 여러가지가 있지만 대표적으로 다음과 같습니다.

  • 문서로 작성되는 그래픽이기 때문에 텍스트 편집기를 통해 제작될 수 있다. 기계판독(Machine-Readable)이 되는 형식이다
    (플래쉬는 이진 바이너리 코드의 실행으로 기계판독이 되지 않는 데이터 입니다)
  • 벡터 그래픽이기 때문에 표현되는 장비 해상도에 영향을 받지 않는다
    (확대, 축소하더라도 원본의 품질을 유지한다)
  • DOM (Document Object Model)을 가지기 때문에 그래픽 요소에 대한 검색, 색인이 가능하다
  • Video, Audio, Raster Image 등 멀티미디어 요소와 통합이 쉽다
  • Animation 지원으로 동적인 그래픽 표현이 가능하다
    (SVG 애니메이션은 SMIL(Synchronized Multimedia Intergration Language)을 정의하는 W3C Synchronized Multimedia(SYMM) 워킹그룹과 협력하여 개발되었습니다.)
  • 웹 표준 기술이기 때문에 별도의 이진 플러그인 없이 웹브라우저에서 바로 동작한다.

[출처] http://svgtutorial.com/svg-browser-support/   

SVG 적용 사례

> 바로가기 : Pinterest (https://www.pinterest.co.kr/)
> 바로가기 : ArcGIS (https://developers.arcgis.com/javascript/3/jssamples/graphics_svg_path.html)
> 바로가기 : Earth NullSchool (https://earth.nullschool.net/)
> 바로가기 : D3-Data Driven Documents (https://d3js.org/)

 

SVG를 이용한 공간정보 시각화

공간정보 분야에서 SVG 적용으로 얻을 수 있는 가장 큰 이점은 SVG Animation 요소 적용으로 실시간으로 변하는 상황의 표현이 쉽다는 것입니다. 아래의 예시는 간단한 SVG 애니메이션 소스 입니다. 

첫번째는 그래픽요소가 가지는 속성을 변경하는 것으로, 실제 공간정보분야에 적용된다면 비즈니스 레이어에 존재하는 객체가 가지는 Business Value에 연동하여 그래픽 표현을 적용하는데 적용될 수 있습니다.
두번째는 객체의 위치를 변경하는 것으로써 이동체(Moving Object)의 실시간 위치를 관제하는 경우 적용될 수 있습니다.
세번째는 지정된 길을 따라가는 예시로 항공기의 궤적, 물류 분야 관제 등 경로 표현에 적용될 수 있습니다.
SVG에서 객체의 형상(Rectagle, Circle, General Path, Group,..)을 정의하는 모든 요소는 애니메이션의 대상이 됨으로 동적인 표현이 가능합니다.

지리좌표계의 적용을 위해 SVG 의 Root Element(<svg>)에 'viewBox' 속성을 지정합니다. 이를 통해 실세계 좌표계(World Coordintate Reference System)를 사용할 수 있습니다. 일반적으로 경위도로 표현되는 좌표를 그대로 이용하기 때문에 GIS App 개발을 위한 별도의 변환 작업이 요구되지 않습니다.

> 다운로드 : svg_gis.html ( 

svg_gis.zip
다운로드

가장 일반적인 방법으로써 공간정보의 시각화를 위하여 Openlayers, leaflet등의 공개 SW가 널리 사용됩니다. 이 들 라이브러리는 D3와 함께 사용되면 동적인 정보 표현을 가능하게 합니다. D3는 HTML, SVG, CSS를 쉽게 사용하도록 도와주는 Wrapper 또는 Helper 라이브러리 입니다. 사용자는 SVG가 정의하는 그래픽 요소에 대한 지식이 없더라도 D3를 통해 쉽게 SVG를 이용할 수 있습니다.

기회가 된다면 본 블로그를 통해 다양한 GIS Data Viewer(OpenLayers, Leaflet, CesiumJS)에서 SVG를 활용하는 사례를 직접 제작하여 게시하겠습니다. 

> 바로가기 : SVG 애니메이션 사례 - 영국 바람 차트 (

https://charts.animateddata.co.uk/ukwind/)

> 바로가기 : leatlet + SVG - TOURISVIS.COM 스키장 안내 지도 (

https://winter.intermaps.com/oetztal?lang=en)

 

 

+ Recent posts