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

 

 

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

이번 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 올리기 결과화면

서비스바로가기



개요

구현된 Earth 사이트 링크

Node.js 와 D3.js 기반으로 구현된 Earth 모듈(nullschool) 에 GFS(Global Forecast System)으로 부터 6시간 간격으로 업데이트되는 전세계 기상예보 데이터를 이용하여 지도(구) 기반으로 표출하는 시스템입니다.

현재는 지상데이터(Surface)를 이용하여 지도 상에 위치에 따른 풍향, 풍속을 나타내며, 흐름을 표출합니다. 미국 기상청으로부터 주기적으로 기상정보를 수집한 후 서비스를 위한 변환을 자동화하는 작업을 진행 중입니다. 향후 게시되는 링크를 통해 다양한 기상정보의 조회 서비스를 제공할 예정입니다.

NullSchool 활용을 위한 설정 등은 하단에 기술된 단락을 참조하십시오. 

 

 

NullSchool 설치 및 셋팅

리눅스 서버에 node.js 와 npm 을 설치한 후에, github 로 부터 "earth" 를 복사하고 관련된 라이브러리 등을 설치합니다. 설치 및 환경 셋팅을 위한 명령어는 다음과 같습니다. 

git clone https://github.com/cambecc/earth cd earth npm install

※ 참조 사이트: https://github.com/cambecc/earth 

earth 모듈 설치가 성공적으로 완료되면, <earth 설치 디렉토리> 로 이동하여 다음과 같은 명령어를 실행하여 웹서버를 실행합니다.

node dev-server.js 8080 

웹서버가 실행되면, 다음과 같은 주소로 접속하여 서비스를 확인할 수 있습니다.

http://localhost:8080

서버는 정적 S3 버킷 호스팅을위한 스탠드 인 역할을 하기 때문에 거의 서버 측 로직을 포함하지 않으며,  "earth/public" 디렉토리에 있는 모든 파일들이 주로 사용됩니다. 주요 소스코드는 "public/index.html" 및 "public/libs/earth/*.js" 를 참조하십시오. 데이터 파일은 "public/data" 디렉토리에 있으며, "data/weather/current" 로 실시간으로 변환되어 전송된 날씨 레이어가 표출됩니니다. 

 

기상 데이터 생성 방법

기상 데이터는 미국 기상청 (National Weather Service)에서 운영하는 GFS (Global Forecast System)에 의해 생성된 데이터를 이용합니다. 예보는 매일 4 번 생성되며 NOMADS (http://nomads.ncep.noaa.gov/)에서 다운로드 할 수 있습니다. 파일은 GRIB2 형식이며 300 개가 넘는 레코드가 있으며, 특정 등온선에서 풍력 데이터를 시각화하기 위해 이러한 기록 중 일부만 필요합니다. 다음과 같은 명령어는 1000 hPa 바람 벡터를 다운로드하고 grib2json 유틸리티를 사용하여 JSON 형식으로 변환합니다. 
YYYYMMDD=<a date, for example: 20190115>
curl "http://nomads.ncep.noaa.gov/cgi-bin/filter_gfs_1p00.pl?file=gfs.t00z.pgrb2.1p00.f000&lev_10_m_above_ground=on&var_UGRD=on&var_VGRD=on&leftlon=0&rightlon=360&toplat=90&bottomlat=-90&dir=%2Fgfs.${YYYYMMDD}00" -o gfs.t00z.pgrb2.1p00.f000

 

grib2json -d -n -o current-wind-surface-level-gfs-1.0.json gfs.t00z.pgrb2.1p00.f000

 

cp current-wind-surface-level-gfs-1.0.json <earth-git-repository>/public/data/weather/current

 ※ grib2json 모듈 참조 사이트: https://github.com/cambecc/grib2json

 

구현된 Earth 사이트 링크

 

+ Recent posts