openlayers 다중 레이어중 원하는 레이어만 선택되도록 설정하는 방법

openlayer를 이용하여 개발을 하다보면 지도위에 여러개의 layer를 등록하게됩니다. 여러개의 레이어중 임의의 레이어만 선택이 가능하게할 수있는 방법을 알려드리겠습니다. 

4행~8행 : 배경지도 생성

9행~16행 : 지도의 위치, zoom등 옵션값 등록

18행~20행 : 레이어 생성

22행 : 지도 등록

30행 : 선택이 가능해야하는 레이어 설정

33행 : select 함수 실행 및 선택가능하도록 설정한 레이어 옵션으로 넣어주기

38행 : select기능 지도에 추가

40행 : 기능확인

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
<script type="text/javascript">
    $(document).ready(function(){
        //레스터 지도
        var vworldRaster = new ol.layer.Tile({
            source: new ol.source.XYZ({
                url: 'http://xdworld.vworld.kr:8080/2d/Base/201802/{z}/{x}/{y}.png'
            })
        });
        var mapView = new ol.View({
            center: [14128579.824512570.74],
            projection : "EPSG:3857",
            zoom : 3,
            minZoom : 3,
            maxZoom : 19,
            extent : [13678546.517133834188.803342414854453.7600595314661.8558898]
        });
        
        var layer1 = createVectorLayer('layer1'truenull);
        var layer2 = createVectorLayer('layer2'truenull);
        var layer3 = createVectorLayer('layer3'truenull);
        
        var map = new ol.Map({        
            logo : false,
            target : 'map',
            layers : [vworldRaster, layer1, layer2, layer3],
            view : mapView
        });
        
        //feature 선택이 가능해야하는 레이어에 set해주기
        layer2.set('selectable'true);
        
        //select 기능 함수
        var selectResultFt = new ol.interaction.Select({
            layers: function(layer) {
                return layer.get('selectable'== true;
            }
        });
        map.addInteraction(selectResultFt);
        
        selectResultFt.on('select'function(evt){
            alert("선택완료");
        });
    });
    //벡터레이어 생성
    function createVectorLayer(id, viewSe, style){
        return new ol.layer.Vector({
            id : id,
            visible : viewSe,
            source : new ol.source.Vector(),
            style : style
        });
    }
</script>
<body>
    <div id="map"></div>
</body>
 
 
 
cs

저는 layer2 라는 id를 넣어준 레이어(19행 참고)에 선택이 가능하도록 설정해주었습니다. 만약 다른레이어가 선택되도록 변경하고 싶으시다면 30행에 set해주는 부분에 변수명을 변경해주시면 될거같습니다.

가장 중요한 부분이 30행부터 38행까지 같습니다. 

오랜만에 글을 올리게 되어 죄송합니다... 앞으로는 좀 더 자주 올리도록 하겠습니다. 새해복 많이 받으세요!!!!

Openlayers3에서 사용자가 지정한 위치를 표시하고 Map을 PDF로 저장하는 방법을 알아보겠습니다.

먼저 결과화면을 확인해보겠습니다.

서비스바로가기

 
 
배경지도는 Openlayers3를 이용하였으며, 지도를 PDF로 저장하기 위해 아래 모듈을 사용하였습니다.

1. mapExport.jsp

 

위에 첨부한 파일은 원하시는 이클립스 디렉토리에 저장하셔서 사용하시면 되겠습니다.

1
<script src="<c:url value='/js/FileSaver/fileSaver.js'/>"></script>
cs

<script></script>부분부터 확인해보도록 하겠습니다. 

    • 3행 : 사용자가 원하는 위치를 클릭시 사용할 마커 이미지 및 정보를 iconStyle에 등록합니다.
    • 11행 : 클릭한 위치의 정보와 마커이미지를 vectorSource에 담아두기 위해 선언합니다.
    • 13행 : 지도에 표시하기 위해 vectorLayer에 vectorSource를 담아두겠습니다.
    • 16행 : 지도 불러오기
    • 24행 : 화면에 표출되는 지도의 좌표정보를 등록하는 곳입니다.
    • 32행 : 지도 클릭 이벤트
    • 38행 : iconFeature에 클릭한 위치의 좌표정보를 등록합니다.
    • 42행 : iconFeature에 3행에서 등록한 이미지 및 정보를 넣어줍니다.
    • 44행 : 42행를 통해 등록한 정보를 vectorSource에 담아줍니다. ==> 담아준 정보는 지도 불러오기 중 지도의 layers부분(21행)을 통해 지도에 표시됩니다.
    • 48행 : 지도부분을 PDF로 Export하는 부분입니다. Export를 위해서 위쪽에 올려드린 첨부파일이 필요합니다.
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
<script type="text/javascript">
$(document).ready(function(){
    var iconStyle = new ol.style.Style({
        image: new ol.style.Icon(({
            anchorXUnits: 'fraction',
            anchorYUnits: 'pixels',
            src: '/images/common/waypoint.png',
            scale: 0.5
        }))
    });
    var vectorSource = new ol.source.Vector();
    
    var vectorLayer = new ol.layer.Vector({
        source: vectorSource
    });
    var map = new ol.Map({
        layers: [
            new ol.layer.Tile({
                source: new ol.source.OSM()
            }),
            vectorLayer
        ],
        target: 'map',
        view: new ol.View({
            center: [1987706.31,4522199.26],
            zoom: 3,
            minZoom: 3,
            maxZoom: 19
        })
    });
    
    map.on('click',function(evt){
        var coordinate = evt.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);
        
    });
    
    document.getElementById('export-png').addEventListener('click'function() {
        map.once('postcompose'function(event) {
            var canvas = event.context.canvas;
            canvas.toBlob(function(blob) {
                saveAs(blob, 'UserMap.png');
            });
        });
        map.renderSync();
    });
});
 
</script>
cs

<body></body>부분입니다.

1
2
3
4
<body>
    <div id="map"></div>
    <a id="export-png" class="btn btn-default"><i class="fa fa-download"></i> Download PNG</a>
</body>
cs

2. 결과화면

Openlayers3에서 전세계 공항의 위치 정보를 이용한 지도 표출을 알아보겠습니다. 먼저 결과화면을 확인해보겠습니다.

 

배경지도는 Openlayers3를 이용하였으며, 공항 위치정보 데이터 또한 Openlayers에서 제공하는 데이터를 이용하였습니다.
원래는 국내공항의 위치정보를 이용하여 기능을 보여드리려 하였지만 비행이동이 화려하게 표현되지 못해 Openlayers에서 제공하는 데이터를 그대로 사용하였습니다. 
 
전세계 공항 정보 .json 데이터는 아래에 첨부파일 다운로드 받으시면 됩니다.
flights.json
다운로드

그리고 위 화면처럼 타원을 그리며 비행기가 날아가도록 하기위해 별도의 모듈을 이용했습니다.

arc.js
다운로드

 

1. Flights.jsp

1
2
3
4
5
6
7
8
9
10
<head>
<title>flights</title>
<meta charset="utf-8">
 
<link type="text/css" rel="stylesheet" href="<c:url value='/css/openlayers/ol.css'/>" />
 
<script src="<c:url value='/js/openlayers/ol.js'/>"></script>
<script src="<c:url value='/js/arc/arc.js'/>"></script>
 
</head>
cs

지금까지 등록된 Openlayers 예제를 보셨다면 소스는 쉽게 이해하실 수 있으실 겁니다. 먼저 <script></script>부분을 살펴보도록하겠습니다.

    • 3행~21행 : Openlayers3 지도 로드
    • 23행 : 비행 이동 경로(라인) 스타일
    • 31행 : 66행을 통해 비행 이동 시작 및 flightsSource에 feature 정보 넣기
    • 40행 : 66행을 통해 비행 이동 도착
    • 66행 : 공항 위치정보 json에 있는 위치정보를 이용하여 시작점/도착점 설정
    • 100행 : 비행이동 경로 지도에 표출
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
<script type="text/javascript">
$(document).ready(function(){
    var map = new ol.Map({
        target: 'map',
        layers: [
            new ol.layer.Tile({
                source: new ol.source.Stamen({
                    wrapX: false,
                    layer: 'toner'
                })
            })
        ],
        view: new ol.View({
            //경도,위도
            center: [1987706.31,4522199.26],
            zoom: 3,
            minZoom: 3,
            maxZoom: 19
        }),
        
      });
    
    var style = new ol.style.Style({
        stroke: new ol.style.Stroke({
            color: '#FF4233',
            width: 2
        })
    });
    
    var flightsSource;
    var addLater = function(feature, timeout) {
        window.setTimeout(function() {
            feature.set('start'new Date().getTime());
            flightsSource.addFeature(feature);
        }, timeout);
    };
    
    var pointsPerMs = 0.1;
    
    var animateFlights = function(event) {
        var vectorContext = event.vectorContext;
        var frameState = event.frameState;
        vectorContext.setStyle(style);
        
        var features = flightsSource.getFeatures();
        for (var i = 0; i < features.length; i++) {
            var feature = features[i];
            if (!feature.get('finished')) {
                var coords = feature.getGeometry().getCoordinates();
                var elapsedTime = frameState.time - feature.get('start');
                var elapsedPoints = elapsedTime * pointsPerMs;
                
                if (elapsedPoints >= coords.length) {
                    feature.set('finished'true);
                }
                
                var maxIndex = Math.min(elapsedPoints, coords.length);
                var currentLine = new ol.geom.LineString(coords.slice(0, maxIndex));
            
                vectorContext.drawGeometry(currentLine);
            }
        }
        map.render();
    };
    
    flightsSource = new ol.source.Vector({
        wrapX: false,
        loader: function() {
            var url = "/js/data/geojson/flights.json";
            fetch(url).then(function(response) {
                return response.json();
            }).then(function(json) {
                var flightsData = json.flights;
                for (var i = 0; i < flightsData.length; i++) {
                    var flight = flightsData[i];
                    var from = flight[0];
                    var to = flight[1];
                    
                    var arcGenerator = new arc.GreatCircle(
                        {x: from[1], y: from[0]},
                        {x: to[1], y: to[0]});
                    
                    var arcLine = arcGenerator.Arc(100, {offset: 10});
                    if (arcLine.geometries.length === 1) {
                        var line = new ol.geom.LineString(arcLine.geometries[0].coords);
                        line.transform(ol.proj.get('EPSG:4326'), ol.proj.get('EPSG:3857'));
                        
                        var feature = new ol.Feature({
                            geometry: line,
                            finished: false
                        });
                        addLater(feature, i * 50);
                    }
                }
            map.on('postcompose', animateFlights);
            });
        }
    });
 
    var flightsLayer = new ol.layer.Vector({
        source: flightsSource,
        style: function(feature) {
            if (feature.get('finished')) {
                return style;
            } else {
              return null;
            }
        }
    });
    map.addLayer(flightsLayer);
});
 
</script>
 
cs

<body></body>및 <style></style>부분입니다.

1
2
3
4
5
6
7
8
9
<style>
#map {
    height:730px;
}
</style>
 
<body>
    <div id="map"></div>
</body>
cs

2. 결과화면

leaflet 배경지도에 Shape File파일을 서버측 코드 없이 웹 브라우저를 통해 GeoJson으로 변환하여 올리는 방법을 알아보겠습니다. 

 

먼저 결과화면을 확인해보겠습니다.

서비스바로가기

 

※ 배경지도 라이브러리

※ shape File을 지도에 올리기 위한 js 파일

(zip포맷으로 올린 Shape File의 압축을 풀고 JavaScript의 인코딩으로 데이터를 서버 측 코드 없이 브라우저를 통해 GeoJson으로 변환 및 표출 모듈)

 

preview.js : Shap File을 GeoJson으로 변환

dbf.js : .Shape File파일의 속성부분 정의
 

1. index.jsp

    • 37행 : leaflet 지도 올리기
    • 40행 : leaflet 배경지도 Vworld로 변경
    • 42행~71행 : 속성정보를 표출
    • 72행~104행 : zip파일 형태의 Shape File을 geojson으로 변경 및 지도에 표출하는 부분
    • 77행 : loadshp 기능 실행 -> preview.js  및 perprocess.js 실행(GeoJson으로 변환 및 Shape File의 .dbf 파일 속성정보 모듈)
 

 

 

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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<!doctype html>
<html lang="ko">
<head>
<title>shp loading</title>
<meta http-equiv="Content-Type" content="charset=UTF-8" />
<meta name="author" content="Gipong">
<meta name="keywords" content="shapefile,dbf,geojson,preview">
 
<link rel="stylesheet" href="<c:url value='/resources/css/leaflet/leaflet.css'/>" />
<link rel="stylesheet" href="<c:url value='/resources/css/semactic/semantic.min.css'/>" />
<link rel="stylesheet" href="<c:url value='/resources/css/demo/demo.css'/>" />
<link rel="stylesheet" href="<c:url value='/resources/css/jquery/jquery-ui.min.css'/>" />
 
<script src="<c:url value='/resources/js/pro4j/pro4j.js'/>"></script>
 
<script src="<c:url value='/resources/js/jquery/jquery-2.1.1.min.js'/>"></script>
<script src="<c:url value='/resources/js/jquery/jquery-ui.min.js'/>"></script>
<script src="<c:url value='/resources/js/leaflet/leaflet.js'/>"></script>
 
<script src="<c:url value='/resources/js/lib/jszip.js'/>"></script>
<script src="<c:url value='/resources/js/lib/jszip-utils.js'/>"></script>
<script src="<c:url value='/resources/js/lib/jszip-utils-ie.js'/>"></script>
 
<script src="<c:url value='/resources/js/semantic/semantic.min.js'/>"></script>
<script src="<c:url value='/resources/js/preprocess/dbf.js'/>"></script>
<script src="<c:url value='/resources/js/preview/preview.js'/>"></script>
 
</head>
<script type="text/javascript" charset="UTF-8">
$(document).ready(function() {
 
    var map = L.map('map').setView([36.3991782860765514129048.150024414], 7),
    file,
    vector;
    L.tileLayer('http://xdworld.vworld.kr:8080/2d/Base/201802/{z}/{x}/{y}.png').addTo(map);
    
    function initVector () {
        vector = L.geoJson([], {
            style: function (feature) {
                return feature.properties.style;
            },
            onEachFeature: function (feature, layer) {
 
                layer.on({click: function(e) {
                    vector.eachLayer(function(l) {
                        vector.resetStyle(l);
                    });
 
                    $('.tbodyContent').remove();
                    var tbody = '<tbody class="tbodyContent">';
                    for (var key in e.target.feature.properties) {
                        tbody +=
                            ('<tr class="center aligned"><td>'+ key + '</td><td>' + e.target.feature.properties[key] + '</td></tr>');
                    }
                    $('#attribute').append(tbody + '</tbody>');
                    $('#attr').fadeIn(300);
                    map.panTo(e.latlng);
 
                    if('setStyle' in e.target) e.target.setStyle({
                        fillColor: '#FF0',
                        fillOpacity: 0.6
                    });
                }});
            }
        }).addTo(map);
    }
    function loadShpZip() {
        var epsg = ($('#epsg').val() == '') ? 4326 : $('#epsg').val(),
        encoding = ($('#encoding').val() == '') ? 'UTF-8' : $('#encoding').val();
        if(file.name.split('.')[1== 'zip') {
            if(file) $('.dimmer').addClass('active');
            loadshp({
                url: file,
                encoding: encoding,
                EPSG: epsg
            }, function(data) {
                var URL = window.URL || window.webkitURL || window.mozURL || window.msURL,
                url = URL.createObjectURL(new Blob([JSON.stringify(data)], {type: "application/json"}));
 
                $('.shp-modal').toggleClass('effect');
                $('.overlay').toggleClass('effect');
                $('#wrap').toggleClass('blur');
 
                vector.addData(data);
                map.fitBounds([
                    [data.bbox[1], data.bbox[0]], [data.bbox[3], data.bbox[2]]
                ]);
                $('.dimmer').removeClass('active');
                $('#preview').addClass('disabled');
                $('#epsg').val('');
                $('#encoding').val('');
                $('#info').addClass('picInfo');
                $('#option').slideUp(500);
            });
        } else {
            alert("ZIP 포맷으로 사용해주십시오.")
            return;
        }
    }
    initVector();
 
    $("#file").change(function(evt) {
        file = evt.target.files[0];
        var fileSize = Math.round(file.size);
        console.log(fileSize);
        if(file.size > 600000) {
            alert("600KB 이햐의 데이터를 이용하여 주십시오.")
            return;
        }else{
            $('#dataInfo').text(' ').append(file.name+' , '+file.size+' kb');
            $('#option').slideDown(500);
            $('#preview').removeClass('disabled');
        }
    });
 
    $('#preview').click(function() {
        loadShpZip();
    });
 
    $('#addZipfile').click(function() {
        $('.shp-modal').toggleClass('effect');
        $('.overlay').toggleClass('effect');
        $('#wrap').toggleClass('blur');
    });
    $('#cancel').click(function() {
        $('.shp-modal').toggleClass('effect');
        $('.overlay').toggleClass('effect');
        $('#wrap').toggleClass('blur');
    });
    $('#removeLayer').click(function() {
        $('#attr').fadeOut(300);
        window.location.reload();
    });
    
    $('#encoding').dropdown();
    
    $("#attr").draggable({ containment: $(this).parent().parent(), scrollfalse, cursor: "move" });
    $('#cancelAttr').click(function(){ $('#attr').slideUp(300); });
});
</script>
    
<body>
    <div id="wrap" class="wrap">
        <div id="map" ></div>
        
        <div id="attr">
            <div class="tableDisplay">
                <table id="attribute" class="ui small celled unstackable table">
                    <thead id="attrHead">
                        <tr class="center aligned">
                            <th>Attribute</th>
                            <th>Value</th>
                        </tr>
                    </thead>
                    <tbody class="tbodyContent">
                    </tbody>
                </table>
            </div>
            <div class="ui red icon button" id="cancelAttr">닫기</div>
        </div>
 
        <footer id="footer">
            <div class="ui page grid">
                <div class="sixteen wide column center aligned">
                    <div id="addZipfile" class="tips large ui positive right labeled icon button">레이어 추가(zip)</div>
                    <div id="removeLayer" class="negative large ui button">레이어 초기화</div>
                </div>
            </div>
        </footer>
    </div>
 
    <div id="shp" class="shp-modal">
        <div class="ui page grid">
            <div class="sixteen wide aligned column">
                <div class="ui form segment">
                    <div class="field">
                        <div class="ui teal fluid labeled icon button upload" id="zipfile" data-variation="inverted large">레이어 불러오기
                            <input type="file" id="file" accept=".zip">
                        </div>
                    </div>
                    <div class="field" id="dataInfo"></div>
    
                    <div id="option">
                        <div class="field ui labeled input" id="epsgField">
                            <div class="ui label">EPSG</div>
                            <input type="text" placeholder="Default : 4326" id="epsg" class="v" onfocus="this.value='';">
                        </div>
                        <div class="field ui labeled input" style="top:-7px;">
                            <div class="ui label" >Encoding</div>
                            <input type="text" placeholder="Encoding UTF-8, EUC-KR, Big5, Big5-HKSCS ... " id="encoding" class="v" onfocus="this.value='';">
                        </div>
                    </div>
                    <div class="two fields" style="padding: 0px 97px;">
                        <div class="field">
                            <div class="ui teal fluid labeled icon disabled button" id="preview"    style="margin: 5px 40px;">레이어 등록</div>
                        </div>
                        <div id="cancel" class="negative right ui button"    style="margin: 5px 40px;">닫기</div>
                    </div>
                </div>
                <div class="ui inverted dimmer">
                    <div class="ui large text loader">데이터를 불러오고있습니다...</div>
                </div>
            </div>
         </div>
     </div>
    <div class="overlay"></div>
 
 
</body>
</html>
 
cs

 


2. 실행 조건

    • Shape File 용량 제한

=> 서버를 이용하지 않고 웹 브라우저를 통해 shapeFile을 GeoJson 형태로 올리기 위해서 Shape File의 용량을 제한

    • EPSG 코드 입력
=>Default로 4326을 설정하였으며, Zip파일 내에 .prj파일이 존재하는 경우, 사용자가 입력한 EPSG로 변경되지 않음 (.prj파일 우선순위)
    • Encoding
=> Encoding을 정확히 입력하여야 속성정보 깨짐이 없음
 
 
3. 결과화면
 
 

Leaflet에서 D3를 이용한 TSP(Traveling Salesman Problem) 활용

서비스바로가기

 

Leaflet에서 D3 라이브러리를 이용하여 TSP를 구현 후, 지도상에 사용자가 지정한 위치를 단 한번만 방문하고 시작점으로 돌아오는 TSP를 구현하였다. 

1.TSP(traveling salesman problem)란?

 

여러 도시들이 있고 한 도시에서 다른 도시로 이동하는 비용이 모두 주어졌을때, 모든 도시들을 단 한번만 방문하고 원래 시작점으로 돌아오는 최소 비용의 이동 순서를 구하는 것이다. 이는 조합 최적화 문제의 일종으로 이 문제는 NP-난해에 속하며, 흔히 계산 복잡도 이론에서 해를 구하기 어려운 문제의 대표적인 예로 많이 다룬다.

참조한 소스가 성능적으로 우수하지는 않습니다. 지도 조회 도구(Leaflet)과 시각화 라이브러리(D3)를 함께 사용하는 예시로 참조하십시오. 인터넷에는 TSP를 해결하는 다양한 응용 사례가 존재합니다.

(참조 : 위키백과 https://ko.wikipedia.org/wiki/%EC%99%B8%ED%8C%90%EC%9B%90_%EB%AC%B8%EC%A0%9C)

 

2. 구현

2.1 leaflet

leaflet을 이용한 웹페이지 지도 발행은 [꿀팁-OpenLayers, Leaflet] > [OpenLayers와 Leaflet에서 Vworld 배경지도 이용하기]을 참고하세요.

D3 라이브러리를 이용하기 위해 leaflet의 지도에 svg 추가가 필요합니다. 프로그웍스 Openlayers 및 Leaflet에서 제공하는 데이터 및 배경지도는  국가공간정보포털 오픈마켓 데이터 및 VWorld를 이용하고 있습니다. <script></script>부분은 아래와 같습니다.

    • 3행 : leaflet 지도 불러오기
    • 4행 : 배경지도 Vworld 활용
    • 12행 : leaflet Map에 svg 추가(D3.js 라이브러리를 사용하기 위해 반드시 추가 필요)
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script type="text/javascript">
$(document).ready(function(){
    var    letMap = L.map('letMap').setView([37.52470308242787126.9234],12);
        L.tileLayer('http://xdworld.vworld.kr:8080/2d/Base/201802/{z}/{x}/{y}.png').addTo(letMap);
        letMap.options.minZoom = 12;
        letMap.options.maxZoom = 12;
        letMap.dragging.disable();
        letMap.touchZoom.disable();
        letMap.doubleClickZoom.disable();
        letMap.scrollWheelZoom.disable();
        
        L.svg().addTo(letMap);
            
});
</script>
cs

 

2.2 D3(Data Drivened Document).js + TSP

D3(Data Drivened Document’).js는 웹에서 데이터를 표현하기에 적합한 도구로 특히, 인터랙티브 데이터 시각화에 많이 사용되고 있는 자바스크립트 기반 라이브러리입니다. D3.js는 HTML, SVG(Scalable Vector Graphics), CSS를 사용해 데이터를 시각적 결과물로 나타낸다. 위 leaflet <script></script> 12행에 leaflet Map에 svg를 추가한 이유가 D3.js 라이브러리를 이용하기 위함이었습니다.

다른방법을 알고 계신다면 알고계신 방법을 사용하셔도 무방하지만 저는 svg를 사용하겠습니다. 아래 <script></script>부분은 d3.js를 이용하여 TSP를 구현한 부분입니다. 소스가 상당히 길지만 정리된 내용을 보시면 금방 이해를 하실 수 있으실 겁니다.

D3.js 및 TSP분석을 위해 underscore.min.js 및 d3.v3.min.js파일을 추가합니다. leaflet map을 이용하기 위해서는 leaflet.js 및 .css파일로 추가하셔야합니다. (참고하기 : 

[꿀팁-OpenLayers, Leaflet] > [OpenLayers와 Leaflet에서 Vworld 배경지도 이용하기])

1
2
3
4
5
<link type="text/css" rel="stylesheet" href="<c:url value='/css/leaflet/leaflet.css'/>" />
 
<script src="<c:url value='/js/D3/d3.v3.min.js'/>"></script>
<script src="<c:url value='/js/leaflet/leaflet.js'/>"></script>
<script src="<c:url value='/js/TSP/underscore.min.js'/>"></script>
cs

D3.js 및 TSP 구현부분을 확인하기 전 알아야 할 부분이 있다.

TSP는 NP-hard 문제에 속하는 것으로 완전 탐색 연결을 위해서는 0(N!)이라는 시간 복잡도가 나온다(Factorial). 이는 10개만 탐색해도 3,628,800개 라는 값이 나오며, 웹페이지에서 구현 시, 페이지가 멈추는 현상이 생긴다.... 그렇기 때문에 포인트 개수에 제한을 두고 개발하였다.(최대 9개 지점 선택 가능)

- 참조 : Factorial( https://ko.wikipedia.org/wiki/%EA%B3%84%EC%8A%B9 )

- NP-Hard란?

비 결정론적 튜링머신으로 다항시간 내에 풀 수 있는 문제로 특정 상태에서 움직일 수 있는 상태의 개수가 하나로 정해져 있지 않은 경우를 말한다.

지금부터 <script></script>부분을 살펴보겠습니다.

    • 2행~12행 : d3.js에서 tsp를 구현하기 위해 사용한 변수 선언
    • 14행 : d3.js를 이용한 TSP 구현 부분
    • 23행 : leaflet map하위의 svg부분을 변수 svg에 선언
    • 23행~42행 : marker와 path 부분 선언
    • 44행 : d3 click 이벤트 선언
    • 60행 : 지도 클릭에 따른 위치정보 추출 및 지도 위 포인트 표시 function 실행
    • 65행 : TSP 실행(RUN)
    • 81행 : TSP 초기화(RESET)
    • 90행 : 지도 클릭에 따른 포인트 표시(시작점은 빨강/그외 지점은 검은색)
    • 119행 : TSP 분석을 통한 path 그리기
    • 137행~151행 : 선택된 위치를 이용한 TSP 비용 계산(Cost)
    • 151행 : 배열에 담은 선택한 포인트를 랜덤으로 순서 생성 및 Cost 계산 function 실행
    • 163행 : TSP 분석에 필요한 최소 비용 및 경로 추출 반복 횟수 만큼(팩토리얼:factorial) 새로운 방문 순서 및 비용 계산(RandomPath 및 Cost부분 function 실행)
    • 176행 : TSP 옵션 정보를 이용한 path, cost 값 정의 및 TSP 완전 탐색이 진행되는 부분
    • 214행~230행 : TSP 반복 횟수 지정 부분(완전탐색 횟수 지정 부분)
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
<script type="text/javascript">
    var tspCount,
        infoText,
        echoText,
        clickCoords,
        featureElement;
    var resultCost = [];    
    var checkRun = false;
    
    var dotscale = 12,
        nodes = []
        cities = [];
        
    (function (d3, _) {
        "use strict"
        
        drawFeatures();  
        
        function drawFeatures() {
            
            var path = d3.geo.path();
            var svg = d3.select("#letMap").select("svg");
            svg.append("svg:defs")
                .append("svg:marker")
                  .attr("id""arrow")
                  .attr("viewBox""0 0 10 10")
                  .attr("refX"15)
                  .attr("refY"5)
                  .attr("markerWidth"6)
                  .attr("markerHeight"6)
                  .attr("orient""auto")
                .append("svg:path")
                  .attr("d""M0,0L10,5L0,10");
    
              svg.append("rect")
                  .attr("class""background")
                .attr("height","686")
                .attr("width","100%")
                  .on('click', clickMap);
              
              var g = svg.append("g")
                .attr("id""baseSVG");
              
              function clickMap () {
                  if(checkRun == true){
                      cities = [];
                    tspCount = null;
                    $('#countInfo').html('0');
                    svg.selectAll('circle').remove();
                    svg.selectAll('path.connection').remove();
                    document.getElementById("echoText").value = "";
                    checkRun = false;
                  }
                  
                  if(cities.length >= 9){
                      alert("위치선택은 9개까지 가능합니다.");
                      return;
                  }
                  
                  cities.push(d3.mouse(this));
                clickCoords = d3.mouse(this);
                drawCities();
            }
              
              function run() {
                if(cities.length<2){
                    alert("2점 이상의 위치를 지도에 표시하세요.");
                    return;
                }
                
                  tspCount = null;
                $('#countInfo').html("TSP 0 번 분석");
                optsNum();
                svg.selectAll('path.connection').remove();
                var answer = sanTsp(cities, {});
                drawPaths(answer.initial.path);
                setTimeout(function () { drawPaths(answer.final.path); }, 1000);
                checkRun = true;
            }
              
            function reset () {
                cities = [];
                tspCount = null;
                $('#countInfo').html('0');
                svg.selectAll('circle').remove();
                svg.selectAll('path.connection').remove();
                document.getElementById("echoText").value = "";
            }
            
            function drawCities() {
                if($(".city").attr("cx"== null && $(".city").attr("cx"== null){
                    svg.selectAll('circle').data(cities).enter()
                    .append('circle')
                        .attr('cx'function (d) { return d[0]; })
                        .attr('cy'function (d) { return d[1]; })
                        .attr('r', dotscale)
                        .attr('fill','red')
                        .attr('stroke','black')
                        .attr('opacity','0.7')                        
                        .attr('class''city');
                }else{
                    
                    svg.selectAll('circle').data(cities).enter()
                        .append('circle')
                            .attr('cx'function (d) { return d[0]; })
                            .attr('cy'function (d) { return d[1]; })
                            .attr('r', dotscale)
                            .attr('fill','black')
                            .attr('stroke','black')
                            .attr('opacity','0.7')
                            .attr('class''city');
                }
                
                echoText = document.getElementById("echoText");
                
                echoText.value += cities.length+"번째 위치를 선택하였습니다. (위치정보"+clickCoords+") \n";
            }
            
            function drawPaths(ipath) {
                var paths = _.map(_.zip(ipath.slice(0,ipath.length-1), ipath.slice(1)), function (pair) {
                    return [cities[pair[0]], cities[pair[1]]]
                }).slice();
    
                svg.selectAll('path.connection').remove();
                svg.selectAll('path.connection').data(paths).enter()
                    .append('path')
                        .attr('d'function(d) {
                        var dx = d[1][0- d[0][0],
                            dy = d[1][1- d[0][1],
                            dr = Math.sqrt(dx * dx + dy * dy);
                        return "M" + d[0][0+ "," + d[0][1+ "A" + dr + "," + dr + " 0 0,1 " + d[1][0+ "," + d[1][1];
                      })
                        .attr('class''connection')
                        .attr("marker-end""url(#arrow)");
            }
 
            function ccCost(c1, c2) {
                return Math.sqrt(Math.pow(c1[0- c2[0], 2+ Math.pow(c1[1- c2[1], 2));
            }
            function sum(arr) {
                return _.reduce(arr, function (x,y){ return x+y; }, 0);
            }
            function pathCost(path) {
                var zipped = _.zip(path.slice(0,path.length-1), path.slice(1));
                
                return sum(_.map(zipped, function (pair) {
                    return ccCost(cities[pair[0]], cities[pair[1]]);
                }));
            }
            
            function randomPath() {
                var n = cities.length
                    ,    path = [0// wlog, begin with 0
                    , rest = _.range(1, n);
 
                while (rest.length > 0) {
                    var i = Math.floor(Math.random() * rest.length);
                    path.push(rest[i]);
                    rest.splice(i, 1);
                }
                return path.concat([0]);
            }
            function doRound(cur) {
                var newpath = randomPath(),
                    newcost = pathCost(newpath);
                if ((newcost < cur.cost)) {
                    return {
                        path: newpath,
                        cost: newcost
                    };
                } else {
                    return cur;
                }
            }
            
            function san(opts) {
                var path = randomPath(),
                    cost = pathCost(path),
                    cur = {
                        path: path,
                        cost: pathCost(path)
                    },
                    answer = {
                        initial: cur
                    },
                    i;
                
                console.log('Starting SAN-TPS', cur);
                var firstCur = cur
                
                if(cities.length == 9){
                    opts.N = opts.N*0.75;
                }
                for (i = 1; i <= opts.N; i++) {
                    
                    cur = doRound(cur);
                    if(cur.cost < firstCur.cost){
                        cur = cur;
                    }else{
                        cur = firstCur;
                    }
                    if(opts.N >= 40320){
                        opts.N = opts.N-100;
                    }
                    
                    document.getElementById("countInfo").innerHTML = "TSP " + i + "번 분석 ";
                }
                
                console.log('Finished SAN-TPS', cur);
                answer.final = cur;
                return answer;
            }
            
            function optsNum(){
                for (var i = cities.length; i >= 1; i--) {
                    if(tspCount == null){
                        tspCount = i;
                    }else{
                        tspCount = tspCount * i;
                    }
                }
            }
            
            function sanTsp(cities, opts) {
                opts = opts || {};
                opts.N = tspCount;
                return san(opts);
            }
    
            d3.select('#run').on('click', run);
            d3.select('#reset').on('click', reset);
    
            console.log('Loaded!');
            
        }
        
    })(d3, _);
</script>
cs

 

2.3 Leaflet + D3(Data Drivened Document).js + TSP <Body></Body>부분

    • 2행 : L.map에서 선언한 Id와 div태그에 사용한 아이디는 동일하여야 한다.
1
2
3
4
5
6
7
8
<body>
    <div id="letMap"></div>
    <h1>Location Info</h1>
    <div id ="countInfo">TSP 0 번 분석</div>
    <textarea id="echoText" rows="10" cols="310"></textarea>
    <button id="run">Run</button>
    <button id="reset">Reset</button>            
</body>
cs

 

3. 테스트 및 결과

테스트 시나리오는 사용자가 임의의 지점은 선택하고 그에 따른 TSP 결과 도출로 진행

구현된 TSP는 별도의 가중치가 없으며, 오직 거리를 이용하여 TSP를 구현하였으며, 직선으로 거리를 표시하고 계산하였다.

3.1 테스트 

1. 임의지점 선택에 따른 위치정보 표시 및 선택 개수 제한 유무

(임의 지점 : 부천시청,.가톨릭대학교성심교정, 매봉산, 서울역, 동덕여자대학교, 한양대학교 서울캠퍼스, 대공원입구, 영등포역, 광명시청 총 9개 지점)  

2. TSP 실행 및 화면 표출

3. TSP 분석 횟수 표시

4. 초기화

 

3.2 결과화면

 

 

시나리오

정보시스템은 다양한 데이터소스를 의미있게 전달하는 것으로 정의할 수 있다. 기온, 미세먼지 등 실시간으로 수치화되어 나타나는 정보는 기본적으로 데이터소스가 DB시스템에 저장된 정보가 나타나는 것이 아니라 센서에서 취득되는 정보라고 할 수 있다. 이를 사용자에게 전달하기 위한 과정은 복잡하지만 임의로 몇 개의 센서를 설치하여 센서의 정상 동작 여부를 모니터링하는 시나리오를 정의했다.

서비스바로가기

목적 및 작업 계획

  • 센서작동현황 버튼을 이용하여 사용자가 지정한 일정한 시간 간격으로 해당 데이터의 정보를 Web Socket을 이용하여 전달
  • Web Socket을 통해 전달 받은 데이터의 상태 변화를 지도 위에 시각화
  • 앞 과정을 통해 그룹 내 모든 사용자는 동일한 화면 공유 가능

구성 요약

1. Soket 서버 : 웹 브라우저와 메세지 교환

      • Web Socket의 기본형태는 아래소스와 같이 Open, Close, Error, Message 구분
      • 46행 : @onMessage를 통해 check Sensor Receiver에서 데이터 정보를 받아 데이터의 상태 정보를 전달
      • 48행 : 새로고침에 따른 중복 메세지 방지를 위해 runCheck 조건문 
      • 59행 : SensorInfo.getSensorInfo()클래스를 통해 데이터의 상태 정보 가져와 check Sensor Receiver에 다시 전달
      • 49행/70행~71행 : 타이머 기능을 통해 지정한 시간 간격으로 정보를 갱신하여 check Sensor Receiver에 전달
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
package egovframework.map.service;
 
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
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.stereotype.Controller;
 
import egovframework.map.web.SensorInfo;
 
@Controller
@ServerEndpoint("/websocket")
public class WSocketServer {
 
    static List<Session> sessionUsers = Collections.synchronizedList(new ArrayList<Session>());
    static Boolean runCheck = false;
 
    @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) throws IOException {
 
        if(runCheck == false){
 
TimerTask task = new TimerTask() {
              @Override
              public void run() {
              String sensorInfo = SensorInfo.getSensorInfo();
               Iterator<Session> itr = sessionUsers.iterator();
                  while (itr.hasNext()) {
                     try {
                         Session session = itr.next();
                        
                         session.getBasicRemote().sendText(sensorInfo);
                        
                     } catch (IOException e) {                        
                         e.printStackTrace();
                    }
                  }
 
              }
         };
        
runCheck = true;
    
         Timer timer = new Timer(true);
         timer.scheduleAtFixedRate(task, 01*1000);
    
};
    }
 
}
 

cs

2. check Sensor Receiver

check Sensor Receiver는 센서의 위치 및 상태 정보를 공유할 수 있는 페이지로 지도 위에 위치한 센서의 현재 상태 및 일정 시간 간격으로 변화 상태를 체크하여 공유할 수 있는 페이지다. check Sensor Receiver 페이지의 소스는 아래와 같다.

2.1 <script></script>부분(openlayers3 및 geojson 데이터 부분)

        • 19행 : QGIS를 통해 제작한 Json파일 경로
        • 36행 : openlayer3 map 불러오기
        • 49행 : 19행에서 지정한 경로의 파일을 geoJson 포맷으로  openlayers map에 올리기
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
<script type="text/javascript">
var map,
    ftData,
    vectorSource,
    vectorSocket,
    vectorLayer,
    d3Data,
    d3Style;
 
var changeFillColor,
    changeStrokeColor;
 
var raster = new ol.layer.Tile({
    source: new ol.source.XYZ({
        url: 'http://xdworld.vworld.kr:8080/2d/Base/201802/{z}/{x}/{y}.png'
    })
});
//geojson 파일 경로
var geoUrl = "/js/data/geojson/sensor_point_3857.json";
 
var ftStyle =  new ol.style.Style({
    image: new ol.style.Circle({
        radius: 10,
        fill: new ol.style.Fill({
            color: "rgba(0, 255, 0, 1)"
        }),
        stroke: new ol.style.Stroke({
            color: "rgba(0, 0, 0, 1)",
            width: 2
        }),
    })
});
 
$(document).ready(function(){
    //openlayers3 map
    map = new ol.Map({
        layers: [raster],
       
        target: 'map',
       
        view: new ol.View({
            center: [14128579.824512570.74],
            zoom: 13.5,
            maxZoom: 19
        })
    });
});
//geojson data 지도 표출
$.getJSON(geoUrl, function( data ) {
    var format = new ol.format.GeoJSON();
    
    vectorSource = new ol.source.Vector({
        features: format.readFeatures(data)
    });
    
    vectorSocket = vectorSource.getFeatures();
    
    vectorLayer = new ol.layer.Vector({
        source: vectorSource,
        style : vectorSource.forEachFeature(function(features){
            
            ftData = features;
 
            var originStyle = ftStyle;
            
            ftData.setStyle(originStyle);
        })
    });
    
    map.addLayer(vectorLayer);
    
});    
 
</script>
 
 
cs

2.2 <script></script>부분(socket 통신 부분)

        • 3행 : Web Socket통신 연결
        • 8행~11행 : Web Socket통신 function 함수 실행
        • 33행 : Web Socket통신을 통해 데이터의 정보를 전달 받아 지도 위에 표출 및 상태 메세지 전송
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
<script type="text/javascript">
              
    var webSocket = new WebSocket("ws://***.***.*.**:8080/websocket2");
          
    var echoText = document.getElementById("echoText");
    echoText.value = "";
        
    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";
webSendMessage();
    };
    function wsCloseConnection(){
        webSocket.close();
    };
    function wsSendMessage(){
        webSocket.send(vectorSocket);
    };
    function wsGetMessage(message){
        pointStyle(message.data);
    };
    function wsClose(message){
       echoText.value += "Disconnect ... \n";
    };
 
    function wserror(message){
        echoText.value += "Error ... \n";
    };
        
    function pointStyle(ft){
        var resultStyle;
        var jsonft = JSON.parse(ft);
        var vectorFts = vectorSource.getFeatures();
            
        for(var i = 0; i < vectorFts.length; i++){
            if(i ==  jsonft.index){
                if(jsonft.code == 1){
                    //노랑
                    changeFillColor = "rgba(255, 255, 0, 1)";
                    echoText.value += i +"번 "+"센서(위치정보 :"+ vectorFts[i].getGeometry().getCoordinates()
                                +")에 이상이 감지되었습니다.(코드번호 :"+jsonft.code+"번 ) \n";
                }else if(jsonft.code == 2){
                    //빨강
                    changeFillColor = "rgba(255, 0, 0, 1)";
                    echoText.value += i +"번"+"센서(위치정보 :"+ vectorFts[i].getGeometry().getCoordinates()
                                +")센서를 점검해주십시오.(코드번호 :"+jsonft.code+"번 ) \n";
                }
                resultStyle =  new ol.style.Style({
                    image: new ol.style.Circle({
                        radius: 10,
                        fill: new ol.style.Fill({
                            color: changeFillColor
                        }),
                        stroke: new ol.style.Stroke({
                            color: "rgba(0, 0, 0, 1)",
                            width: 2
                        }),
                    })
                });
                    
            }else{
                resultStyle = ftStyle;
            }    
            vectorSource.getFeatures()[i].setStyle(resultStyle);
        };
    };        
</script>
cs

2.3 <body></body>부분

        • 5행 : openlayers map을 화면에 표출하기 위한 div태그 id 선언
        • 12행 : 데이터 작동 현황 시작 버튼 생성(Web Socket실행)
        • 49행 : Web Socket통신 종료
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="container-fluid">
    <div class="row content">
        <div class="col-sm-6" style"border-right: 1px solid;">
            <h1>Sensor Location</h1>
            <div id="map"></div>
        </div>
        <div class="col-sm-6" style"border-left: 1px solid;">
            <h1>Emergency Message</h1>
            <textarea id="echoText" rows="500" cols="700"></textarea>            
        </div>
    </div>
</div>
cs

3. 실행 화면 및 사용법

테스트는 IE, 크롬, Microsoft Edge에서 진행하였으며, check Sensor Receiver 페이지 접속 시 센서의 상태정보가 실시간으로 반영되어 센세의 상태정보가 변경된다.

  • 노란색 : 센서 상태 이상감지(코드번호 1번)
  • 빨간색 : 센서 점검 필요(코드번호 2번)

1. check Sensor Receiver 페이지를 IE, 크롬, Microsoft Edge에서 접속한다.

2. 우측 메세지창<textarea id="echoText" rows="500" cols="700"></textarea>    에 센서의 위치정보 및 이상 감지 메세지가 표출되는지 확인한다.

3.1 테스트 결과 화면

 

서비스바로가기

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. 결과화면

안녕하세요 캡틴개구리입니다.

이번 D3 라이브러리를 이용하기위해 구글링을 하던 중 제가 느낀점은 한국에서 제공하는 데이터를 이용한 예제가 찾을 수 없었다는 점입니다.... 대부분 미국이었습니다. 

그래서 이번에 국가공간정보포털에서 제공하는 데이터와 VWORLD 지도를 이용하여 한국 예제를 만들었습니다. 

먼저 Openlayers3와 Leaflet에서 D3 TopoJson 이용하기 결과화면부터 보겠습니다.

서비스바로가기


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

필수 라이브러리

OpenLayers3 :  https://openlayers.org/ 

Leaflet : http://leafletjs.com/

D3 : https://d3js.org/d3.v3.min.js

TopoJson : https://d3js.org/topojson.v1.min.js


참고사항 : D3 라이브러리 상위버전이 존재합니다만... 상위버전 라이브러리를 이용할 경우... openlayers3에서 TopoJson을 제대로 가져오지 못하였습니다......머리속에 물음표만 나오지만 저는Openlayers3 example에서 사용한 버전의 라이브러리를 이용하였습니다. (위에 표시된 라이브러리 이용하시면 정상적으로 지도에 데이터를 표출 하실 수 있습니다.)


선택 라이브러리

VWORLD(배경지도) :  http://map.vworld.kr



D3.js는 Data-Driven Documents의 약어로 데이터를 렌더링할 수 있는(그릴 수 있는, 다룰수 있는) 웹 기반 문서 라이브러리로 간단하게 설명하면 데이터 시각화 라이브러리입니다. 또한 D3에서는 SVG, GeoJSON, TopoJSON 등의 포맷을 이용하실 수 있습니다.


오늘은 TOPOJSON을 이용하여 Openlayers3(이하 Ol3)와 Leaflet 지도위에 데이터를 올려보도록 하겠습니다. 



1. TOPOJSON 데이터 생성


D3라이브러리를 이용하기 위해서는 TOPOJSON 형태의 데이터가 필요합니다.


먼저 국가공간정보포털 오픈마켓(http://data.nsdi.go.kr/dataset)에서 연속지적도_서울 데이터를 다운로드 후, 여의도 부분만을 GeoJSON으로 변환하였습니다. 


데이터  : 연속지적도_전국 > LSMD_CONT_LDREG_서울.zip


좌표계 : EPSG : 5174 

 * 참고사항 : 좌표계를 정확하게 하기위해 저는 QGIS에서 제공하는 5174를 사용하지 않고 사용자정의 5174를 생성하여 사용하였습니다.(https://www.osgeo.kr/17) QGIS에서 제공하는 좌표계와 OSGeo 지부에서 제공하는 좌표계의 임계값이 미세하게 차이가 있어서 사용자정의로 좌표계를 생성하였습니다.


GeoJSON 추출 좌표계 : WGS84 EPSG 4326


간단한 QGIS 사용법은 국가공간정보포털 > 자료실에 매뉴얼이 존재합니다.




QGIS를 이용하여 저장한 GeoJSON을 위에서 언급한 TopoJSON으로 변경합니다. 구글에서 GeoJSON to TopoJSON을 검색하셔서 이용하셔도  되고 위에 언급해드린 링크를 이용하여 바로 접속하셔도 됩니다. 개인적으로 변경하는 방법이 있으시다면 그것도 좋습니다.


TopoJSON으로 데이터를 변경하셨다면 기본작업은 끝이났습니다.


2. 라이브러리 추가


이제 OL3와 Leaflet에서 D3를 이용하기 위해 라이브러리를 추가해보도록 하겠습니다. 추가하는 라이브러리는 아래와 같습니다. 


1
2
3
4
5
6
<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/leaflet/leaflet.css'/>" />
<script src="<c:url value='/js/openlayers/ol.js'/>"></script>
<script src="<c:url value='/js/leaflet/leaflet.js'/>"></script>
<script src="<c:url value='/js/D3/d3.v3.min.js'/>"></script>
<script src="<c:url value='/js/D3/topojson.v1.min.js'/>"></script>
cs


3. Openlayers3


OL3에서 D3를 이용하기위해서 제가 참고한 자료는 Openlayers3 example에 있는 d3 Integration입니다. 먼저 <script></script>부분을 먼저 보겠습니다.


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
<script>
    //지도띄우기
    var olMap8 = new ol.Map({
        layers: [
            new ol.layer.Tile({
                source: new ol.source.XYZ({
                    url: 'http://xdworld.vworld.kr:8080/2d/Base/201802/{z}/{x}/{y}.png'
                })
            })
        ],
       
        target: 'olMap8',
       
        view: new ol.View({
             center: [14129600.824512500.74],
            maxZoom: 19,
            zoom: 14
        })
    });
    var topoUrl8 = "/js/data/topojson/progworks_yeouido_4326.json";
    
    d3.json(topoUrl8, function(error, topology) {
        var features = topojson.feature(topology, topology.objects.collection);
  
        console.log("Ol3_topojson", topology)
        
        var canvasFunction = function(extent, resolution, pixelRatio,size, projection) {
            var canvasWidth = size[0];
            var canvasHeight = size[1];
 
            var canvas = d3.select(document.createElement('canvas'));
            canvas.attr('width', canvasWidth).attr('height', canvasHeight);
 
            var context = canvas.node().getContext('2d');
 
            var d3Projection = d3.geo.mercator().scale(1).translate([00]);
            var d3Path = d3.geo.path().projection(d3Projection);
 
            var pixelBounds = d3Path.bounds(features);
            var pixelBoundsWidth = pixelBounds[1][0- pixelBounds[0][0];
            var pixelBoundsHeight = pixelBounds[1][1- pixelBounds[0][1];
 
            var geoBounds = d3.geo.bounds(features);
            var geoBoundsLeftBottom = ol.proj.transform(geoBounds[0], 'EPSG:4326', projection);
            var geoBoundsRightTop = ol.proj.transform(geoBounds[1], 'EPSG:4326', projection);
            var geoBoundsWidth = geoBoundsRightTop[0- geoBoundsLeftBottom[0];
            if (geoBoundsWidth < 0) {
                geoBoundsWidth += ol.extent.getWidth(projection.getExtent());
            }
            var geoBoundsHeight = geoBoundsRightTop[1- geoBoundsLeftBottom[1];
 
            var widthResolution = geoBoundsWidth / pixelBoundsWidth;
            var heightResolution = geoBoundsHeight / pixelBoundsHeight;
            var r = Math.max(widthResolution, heightResolution);
            var scale = r / (resolution / pixelRatio);
 
            var center = ol.proj.transform(ol.extent.getCenter(extent),projection, 'EPSG:4326');
            d3Projection.scale(scale).center(center).translate([canvasWidth / 2, canvasHeight / 2]);
            d3Path = d3Path.projection(d3Projection).context(context);
            d3Path(features);
            context.stroke();
 
            return canvas[0][0];
        };
 
        var layer = new ol.layer.Image({
            source: new ol.source.ImageCanvas({
                canvasFunction: canvasFunction,
                projection: 'EPSG:3857'
            })
        });
        
        olMap8.addLayer(layer);
        
    });
</script>
cs


3행 ~ 19행 : OL3 지도부분입니다.

4행 : 저는 WVORLD 배경지도 사용

14행 ~17행 : 지도 중심, 최대확대 및 지도 로딩 레벨 설정

20행 : TopoJSON 데이터의 경로

22행 : d3.json( TopoJSON경로, function(error, TopoJSON의 Type)


위 그림이 제가 사용한 TopoJSON 데이터입니다. "type" : "Topology" 부분을 TopoJSON Type부분에 입력하시면됩니다. 

몇몇 다른 예제에서는 설명이 없이 값이 다 다르게 들어가 있어서 무엇을 입력해야되나 고민하다가 제가 생각한 결론은 TopoJSON의 Type 이름을 넣자 였습니다... 


27행 ~ 64행 : OL3에서 D3의 TopoJSON을 사용할 수 있도록 변경 및 canvasFunction 변수에 값 지정

66행 ~ 73행 : D3 라이브러리를 통해 변경한 데이터를 OL3 라이브러리를 이용하여 OL3 map에 add

OL3에서 D3 라이브러리를 이용하실때 가장 신경쓰셔야할 부분이 22행 부분이라 생각됩니다. 나머지는 example에 있는부분이라 별도의 변경이 필요없습니다만 22행 부분은 사용자에 맡게 변경이 필요한 부분이기 때문입니다.


다음은 <body></body>입니다.

1
2
3
<body>
    <div class="olMap" id="olMap8"></div>
</body>
cs


저의 경우, 여러개의 OL3와 Leaflet 작업을 하나의 HTML에서 진행하기 때문에 클래스를 넣었지만 <div></div>안에 id만 넣으셔도 무방합니다.


4. Leaflet

Leaflet 홈페이지에 가시면 geoJSON을 바로 이용할 수 있는 방법도 있습니다. 참고해보세요 Leaflet은 OL3와 비교하여 소스가 간단합니다. 먼저 <script></script>부분을 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
//leaflet 지도 띄우기
    var letMap8 = L.map('letMap8').setView([37.52470308242787126.9234],14)
    
    //Vworld Tile 변경
    L.tileLayer('http://xdworld.vworld.kr:8080/2d/Base/201802/{z}/{x}/{y}.png').addTo(letMap8);
    
    //geoJson
    layer8 = L.geoJson(null, { style: { color: '#333', weight: 1 }})
    //geoJson map add
    letMap8.addLayer(layer8)
    
    //TopoJSON 
    var topoUrl8 = "/js/data/topojson/progworks_yeouido_4326.json";
 
    //d3활용 topoJson 파일 불러오기 및 geoJson 전달
    d3.json(topoUrl8, function(error, topology) {
        var topoYeouido8 = topojson.feature(topology, topology.objects.collection)
        layer8.addData(topoYeouido8);
        console.log("Leaflet_topojson", topology)
    })
</script>
cs


Leaflet에서는 TopoJSON 데이터를 적용하기위해서는 결론적으로 GeoJSON으로 변경하여 add시킵니다. 다시말해 굳이 TopoJSON으로 변경할 필요가 없다는겁니다.... 전 OL3를 다 끝내고 알았습니다.......

3행 : Leaflet 지도 올리기

6행 : VWORLD로 지도 변경

9행 : GeoJSON 선언 및 스타일 지정

11행 : Leaflet 지도에 GeoJSON 데이터 add

14행 : TopoJSON 데이터 경로

17행 ~ 21행 : D3 라이브러리를 이용하여 TopoJSON 데이터를 읽어오고 9행으로 값 전달

17행 : OL3 부분에서 언급한것과 동일합니다. d3.json( TopoJSON경로, function(error, TopoJSON의 Type)


다음은 <body></body>부분입니다.

1
2
3
<body>
    <div class="letMap" id="letMap8"></div>
</body>
cs


5. OL3와 Leaflet에서 D3 TopoJSON 올리기 결과화면

서비스바로가기



안녕하세요 캡틴개구리입니다.

이번시간은 Openlayers3(OL3)와 Leaflet에 User Control(거리재기)을 올려보겠습니다.

그전에 결과화면부터 보겠습니다.

서비스바로가기


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

OpenLayers3 :  https://openlayers.org/ 

Leaflet : http://leafletjs.com/

VWORLD :  http://map.vworld.kr



거리재기 기능을 이용하기 위해서는 Openlayers3의 경우 별도의 라이브러리 추가가 필요없지만, Leaflet의 경우 거리재기를 이용하기 위해 별도의 라이브러리가 필요합니다. 


1. Openlayers3 User Control(거리재기 & 면적재기) 추가

활용라이브러리 : ol.Map, ol.layer.Tile, ol.source.Vector, ol.layer.Vector, ol.geom.Polygon, ol.geom.LinString, ol.View, ol.interaction.Draw, ol.style.Style, ol.style.Fill, ol.style.Stroke, ol.style.Circle, ol.Overlay


먼저 Openlayers3(이하 OL3)  <script></script>부분입니다. 참고로 OL3를 이용하기 위해서는 라이브러리 추가합니다.


1
2
3
4
 
<link type="text/css" rel="stylesheet" href="<c:url value='/css/openlayers/ol.css'/>" />
 
<script src="<c:url value='/js/openlayers/ol.js'/>"></script>
cs


OL3 지도 띄우기


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
<script type="text/javascript">
//openlayers 지도 띄우기
    //레스터 지도
    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 vectorSource = new ol.source.Vector();
    //벡터스타일
    var vector = new ol.layer.Vector({
        source: vectorSource,
        style: new ol.style.Style({
            fill: new ol.style.Fill({
                color: 'rgba(255, 255, 255, 0.2)'
            }),
            stroke: new ol.style.Stroke({
                color: 'rgba(0, 0, 255, 1.0)',
                width: 2
            }),
            image: new ol.style.Circle({
                radius: 7,
                fill: new ol.style.Fill({
                    color: '#ffcc33'
                })
            })
        })
    });
    
    //지도띄우기
    var map = new ol.Map({
        
        layers: [raster,vector],
       
        target: document.getElementById('map'),
       
        view: new ol.View({
          center: [14128579.824512570.74],
          maxZoom: 19,
          zoom: 14
        })
    });
</script>
cs

다음은 User Control 거리재기를 가져오도록 하겠습니다. 거리재기의 경우, OL3에서 기본적으로 제공하는 기능이며 또한 거리재기와 함께 면적을 구하는 기능을 함께 보여드리겠습니다.

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
171
172
173
174
175
176
177
178
179
180
181
182
<script type="text/javascript">
 
    //거리재기
    var sketch;
    var helpTooltipElement;
    var helpTooltip;
    var measureTooltipElement;
    var measureTooltip;
    var continuePolygonMsg = 'Click to continue drawing the polygon';
    var continueLineMsg = 'Click to continue drawing the line';
    
    var pointerMoveHandler = function(evt) {
      if (evt.dragging) {
        return;
      }
      /** @type {string} */
      var helpMsg = 'Click to start drawing';
    
      if (sketch) {
        var geom = (sketch.getGeometry());
        if (geom instanceof ol.geom.Polygon) {
          helpMsg = continuePolygonMsg;
        } else if (geom instanceof ol.geom.LineString) {
          helpMsg = continueLineMsg;
        }
      }
    
      helpTooltipElement.innerHTML = helpMsg;
      helpTooltip.setPosition(evt.coordinate);
    
      helpTooltipElement.classList.remove('hidden');
    };
      
    map.on('pointermove', pointerMoveHandler);
 
    map.getViewport().addEventListener('mouseout'function() {
      helpTooltipElement.classList.add('hidden');
    });
 
    var typeSelect = document.getElementById('type');
 
    var draw;
    var formatLength = function(line) {
        var length;
        length = Math.round(line.getLength() * 100/ 100;
        console.log(length);
        var output;
        if (length > 100) {
          output = (Math.round(length / 1000 * 100/ 100+
            ' ' + 'km';
        } else {
          output = (Math.round(length * 100/ 100+
            ' ' + 'm';
        }
        console.log(output);
        return output;
    };
    
    var formatArea = function(polygon) {
        var area;
        area = polygon.getArea();
        var output;
        if (area > 10000) {
          output = (Math.round(area / 1000000 * 100/ 100+
              ' ' + 'km<sup>2</sup>';
        } else {
          output = (Math.round(area * 100/ 100+
              ' ' + 'm<sup>2</sup>';
        }
        return output;
    };
      
    function addInteraction() {
        var type = (typeSelect.value == 'area' ? 'Polygon' : 'LineString');
        draw = new ol.interaction.Draw({
            source: vectorSource,
            type: (type),
            style: new ol.style.Style({
                fill: new ol.style.Fill({
                color: 'rgba(255, 255, 255, 0.2)'
                }),
                stroke: new ol.style.Stroke({
                    color: 'rgba(0, 0, 0, 0.5)',
                    lineDash: [1010],
                    width: 2
                }),
                image: new ol.style.Circle({
                    radius: 5,
                    stroke: new ol.style.Stroke({
                        color: 'rgba(0, 0, 0, 0.7)'
                    }),
                    fill: new ol.style.Fill({
                    color: 'rgba(255, 255, 255, 0.2)'
                    })
                })
            })
        });
        map.addInteraction(draw);
 
    
        createMeasureTooltip();
        createHelpTooltip();
        
        var listener;
        draw.on('drawstart'function(evt) {
            sketch = evt.feature;
            var tooltipCoord = evt.coordinate;
            
            listener = sketch.getGeometry().on('change'function(evt) {
                var geom = evt.target;
                var output;
                if (geom instanceof ol.geom.Polygon) {
                    output = formatArea(geom);
                    tooltipCoord = geom.getInteriorPoint().getCoordinates();
                } else if (geom instanceof ol.geom.LineString) {
                    output = formatLength(geom);
                    tooltipCoord = geom.getLastCoordinate();
                }
                measureTooltipElement.innerHTML = output;
                measureTooltip.setPosition(tooltipCoord);
            });
        }, this);
        
        draw.on('drawend'function() {
            measureTooltipElement.className = 'tooltip tooltip-static';
            measureTooltip.setOffset([0-7]);
 
            sketch = null;
 
            measureTooltipElement = null;
 
            createMeasureTooltip();
            ol.Observable.unByKey(listener);
        }, this);
    }
    
    function createHelpTooltip() {
        if (helpTooltipElement) {
          helpTooltipElement.parentNode.removeChild(helpTooltipElement);
        }
        helpTooltipElement = document.createElement('div');
        helpTooltipElement.className = 'tooltip hidden';
        helpTooltip = new ol.Overlay({
          element: helpTooltipElement,
          offset: [150],
          positioning: 'center-left'
        });
        map.addOverlay(helpTooltip);
    }
    
    function createMeasureTooltip() {
        if (measureTooltipElement) {
          measureTooltipElement.parentNode.removeChild(measureTooltipElement);
        }
        measureTooltipElement = document.createElement('div');
        measureTooltipElement.className = 'tooltip tooltip-measure';
        measureTooltip = new ol.Overlay({
          element: measureTooltipElement,
          offset: [0-15],
          positioning: 'bottom-center'
        });
        map.addOverlay(measureTooltip);
    }
    
    /* function removeInteractionMeasure() {
        vector.getSource().clear(measureTooltipElement);
        map.removeInteraction(draw);
    }     */
 
    typeSelect.onchange = function() {
        vector.getSource().clear(measureTooltipElement);
        map.removeInteraction(draw);
        map.removeOverlay(measureTooltip);
        $('.tooltip-static').remove();
        addInteraction();
    };
    addInteraction();
 
</script>
 
 
 
cs

12번행~38행 pointerMoveHandler는 거리재기 및 면적재기 기능이 활성화 되었을 경우, 그에 맞는 ToolTip 메세지를 제공하는 부분

40행 : id를 이용하여 선택된 기능의 Type 확인

43행~ 54행 : 거리재기 단위 변경 및 output 제공 

59행~71행  : 면적재기 단위 변경 및 output 제공 

73행~135행 : 거리재기 및 면적재기 기능 구현 부분

137행~176행 : 기능에 따른 해당 Tooltip 제공

OL3 User Control 거리재기 및 면적재기 <body></body>입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body>
    <div class="container-fluid">
        <div class="row content">
            <div class="col-sm-6" style="border-right: 1px solid; height: 700px;">
                <h1>Oepnlayers 3</h1>
                <div id="map"></div>
            </div>
        </div>
        <div class="row content">
            <form class="form-inline" style="padding: 13px;">
                <label>Measurement type &nbsp;</label>
                    <select id="type">
                      <option value="length">거리재기</option>
                      <option value="area">면적재기</option>
                    </select>
            </form>            
        </div>
    </div>    
</body>
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
<style>
/* 거리재기 스타일_openlayers3 */
.tooltip {
  position: relative;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 4px;
  color: white;
  padding: 4px 8px;
  opacity: 0.7;
  white-space: nowrap;
}
.tooltip-measure {
  opacity: 1;
  font-weight: bold;
}
.tooltip-static {
  background-color: #ffcc33;
  color: black;
  border: 1px solid white;
}
.tooltip-measure:before,
.tooltip-static:before {
  border-top: 6px solid rgba(0, 0, 0, 0.5);
  border-right: 6px solid transparent;
  border-left: 6px solid transparent;
  content: "";
  position: absolute;
  bottom: -6px;
  margin-left: -7px;
  left: 50%;
}
.tooltip-static:before {
  border-top-color: #ffcc33;
}
    
</style>
cs

2. Leaflet User Control(거리재기) 추가

활용라이브러리 : L.map, L.tileLayer, L.control.measure


위에서 언급드린것 처럼 Leaflet에서 거리재기를 이용하기 위해서는 라이브러리가 필요합니다. 

leaflet.measure.css

leaflet.measure.js


1
2
3
<script src="<c:url value='/js/leaflet/leaflet.js'/>"></script>
<script src="<c:url value='/js/leaflet/leaflet.measure.js'/>"></script>
<link type="text/css" rel="stylesheet" href="<c:url value='/css/leaflet/leaflet.measure.css'/>" />
cs


먼저 leaflet  <script></script>부분입니다.


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
 <script>
//leaflet 지도 띄우기
var leafletMap = L.map('leafletMap').setView([37.5247030824278714129046.928310394],14)    
//Vworld Tile 변경
L.tileLayer('http://xdworld.vworld.kr:8080/2d/Base/201802/{z}/{x}/{y}.png').addTo(leafletMap);
    
var plugin = L.control.measure({
    //  control position
    position: 'topleft',
    //  weather to use keyboard control for this plugin
    keyboard: true,
    //  shortcut to activate measure
    activeKeyCode: 'M'.charCodeAt(0),
    //거리재기 초기화 코드  //shortcut to cancel measure, defaults to 'Esc'
    cancelKeyCode: 27,
    //  line color
    lineColor: 'red',
    //  line weight
    lineWeight: 5,
    //  line dash
    lineDashArray: '6, 6',
    //  line opacity
    lineOpacity: 1,
    //  distance formatter
    // formatDistance: function (val) {
    //   return Math.round(1000 * val / 1609.344) / 1000 + 'mile';
    // }
}).addTo(leafletMap)
</script>
cs


OL3에 비해 매우 간략합니다. 이유는 라이브러리를 이용하였기 때문이라 생각됩니다. 

이번에도 지도는 VWORLD를 이용했습니다.

3행~ 5행 : 지도 띄우기

7행~28행 : 거리재기를 위한 기능 부분으로 <script>부분에서는 옵션값 부분만 설정합니다. 그리고 Leaflet에서는 KeyCode라는것을 사용하는데 

15행에 보시면 cancelKeyCode : 27은 ESC를 이용하여 기능을 초기화 시키는 코드입니다. 

다음은 <body></body>입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
    <div class="container-fluid">
        <div class="row content">
            <div class="col-sm-6" style="border-left: 1px solid; height: 700px;">
                <h1>leaflet</h1>
                <div id="leafletMap"></div>
            </div>
        </div>
    </div>
</body>
<style>
/* 거리재기_leaflet */
#leafletDt{
    text-align: center;
    padding-left: 283px;
}  
    
</style>
cs


결과화면은 아래와 같습니다.


서비스바로가기

안녕하세요 캡틴개구리입니다.

이번시간은 Openlayers3(OL3)와 Leaflet에 Feature 이벤트 연동을 알아보겠습니다.

Feature Event 요약 기능

 - 지도 마커 클릭 시, 팝업 창 호출

 - 지도 클릭 시, 좌표정보 호출


그전에 결과화면부터 보겠습니다.

서비스바로가기


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

OpenLayers3 :  https://openlayers.org/ 

Leaflet : http://leafletjs.com/

VWORLD :  http://map.vworld.kr



1. Openlayers3 Feature Event 추가


활용라이브러리 : ol.Map, ol.layer.Tile, ol.source.Vector, ol.layer.Vector, ol.Feature, ol.geom.Point, ol.style.Style, ol.style.Icon, ol.Overlay 


먼저 Openlayers3(이하 OL3)  <script></script>부분입니다.


OL3 지도 및 마커 띄우기


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
<script>
    //openlayers 지도 띄우기
    var iconFeature = new ol.Feature({
        //아이콘 위치좌표
        geometry: new ol.geom.Point([14129600.824512500.74]),
        name'I am a Progworks.',
    });
 
    var iconStyle = new ol.style.Style({
        image: new ol.style.Icon(/** @type {olx.style.IconOptions} */ ({
            anchor: [146],
            anchorXUnits: 'fraction',
            anchorYUnits: 'pixels',
            src: 'https://openlayers.org/en/v3.20.1/examples/data/icon.png'
        }))
    });
 
    iconFeature.setStyle(iconStyle);
    var vectorSource = new ol.source.Vector({
        features: [iconFeature]
    });
 
    var vectorLayer = new ol.layer.Vector({
        source: vectorSource
    });
     
    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 map = new ol.Map({
           
        layers: [raster,vectorLayer],
          
        target: document.getElementById('map'),
          
        view: new ol.View({
          center: [14129600.824512500.74],
          maxZoom: 19,
          zoom: 14
        })
    });
</script>
cs

다음은 Feature Event를 가져오도록 하겠습니다. 참고로 OL3에서는 마커 표시와 지도의 좌표정보 불러오기가 별도의 example로 존재합니다.

각각의 example를 하나로 합치면서 OL3에서 제공하는 소스와는 조금 다르다는점 참고하시면 좋을것 같습니다.

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
<script>
    var element = document.getElementById('popup');    
    var coordElement = document.getElementById('olPopup');
    
    var popup = new ol.Overlay({
        element: element,
            positioning: 'bottom-center',
            stopEvent: false,
            offset: [-18-50]
    });
    map.addOverlay(popup);
    
    map.on('click'function(evt) {
        var feature = map.forEachFeatureAtPixel(evt.pixel,function(feature) {
            return feature;
        });
        if (feature) {
            
            var coordinates = feature.getGeometry().getCoordinates();
            popup.setPosition(coordinates);
            $(element).popover({
              'placement''top',
              'animation'false,
              'html'true,
              'content': feature.get('name')
            });
            $(element).popover('show');
            $(coordElement).popover('destroy');
        } else {
            $(element).popover('destroy');
        }
    });
    
    var olPopup = new ol.Overlay({
        element: document.getElementById('olPopup')
    });
    map.addOverlay(olPopup);
 
    map.on('click'function(evt) {
        var feature = map.forEachFeatureAtPixel(evt.pixel,function(feature) {
            return feature;
        });
        
        if(feature == null){
            var coordinate = evt.coordinate;
            var hdms = ol.coordinate.toStringHDMS(ol.proj.transform(coordinate, 'EPSG:3857''EPSG:4326'));
    
            $(coordElement).popover('destroy');
            olPopup.setPosition(coordinate);
            $(coordElement).popover({
                'placement''top',
                'animation'false,
                'html'true,
                'content''<p>현재 위치 좌표정보</p><code>' + hdms + '</code>'
            });
            $(coordElement).popover('show');
        } else{
            $(coordElement).popover('destroy');
        }
    });
 
        // change mouse cursor when over marker
    map.on('pointermove'function(e) {
        if (e.dragging) {
            $(element).popover('destroy');
            return;
        }
        var pixel = map.getEventPixel(e.originalEvent);
        var hit = map.hasFeatureAtPixel(pixel);
        map.getTarget().style.cursor = hit ? 'pointer' : '';
    });
</script>
cs

5행 : 마커 클릭 시, 나타나는 팝업의 위치 구현

13행~32행 : OL3 map 클릭 이벤트 (마커)

34행 : 지도 클릭 시, 좌표정보 팝업의 위치구현

39행~60행 : OL3 map 클릭 이벤트 (지도)

63행~71행 : 마커 위로 마우스 이동 시 포인터 활성화 및 지도 좌표정보 팝업 숨김 구현

OL3 Feature Event <body></body>입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
    <div class="container-fluid">
        <div class="row content">
            <div class="col-sm-6" style="border-right: 1px solid;">
                <h1>Oepnlayers 3</h1>
                <div id="map" class ="map">
                    <div id="popup"></div>
                </div>
                <div style="display: none;">
                    <div id="olPopup" title="Welcome to ol3"></div>
                </div>
            </div>
        </div>
    </div>
</body>
cs


2. Leaflet Feature Event

활용라이브러리 : L.map, L.tileLayer, L.marker, L.popup


먼저 leaflet  <script></script>부분입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
//leaflet 지도 띄우기
    var leafletMap = L.map('leafletMap').setView([37.5247030824278714129046.928310394],14)
    //Vworld Tile 변경
    L.tileLayer('http://xdworld.vworld.kr:8080/2d/Base/201802/{z}/{x}/{y}.png').addTo(leafletMap);
    
    var marker = L.marker([37.5247030824278714129046.928310394]).addTo(leafletMap)
        .bindPopup("I am a Progworks.").openPopup();
    
    var leafletPopup = L.popup();
    
    function onMapClick(e) {
        leafletPopup
            .setLatLng(e.latlng)
            .setContent("현재 위치 좌표정보 <br>" + e.latlng.toString())
            .openOn(leafletMap);
    }
 
    leafletMap.on('click', onMapClick);
});
</script>
cs


이번에도 지도는 VWORLD를 이용했습니다.

3행~ 5행 : 지도 띄우기
7행~8행 : 마커 위치 및 내용 넣기
10행 : 팝업
12행 : 클릭이벤트

다음은 <body></body>입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
    <div class="container-fluid">
        <div class="row content">
            <div class="col-sm-6" style="border-left: 1px solid;">
                <h1>leaflet</h1>
                <div id="leafletMap"></div>
            </div>
            <div class="col-sm-6" style="border-right: 1px solid; border-top: 1px solid;">
                
            </div>
            <div class="col-sm-6" style="border-left: 1px solid; border-top: 1px solid;">
                
            </div>
        </div>
    </div>
</body>
cs

결과화면은 아래와 같습니다.

서비스바로가기

+ Recent posts