기초/자바스크립트

[Javascript] OpenLayers + GeoServer + PostGIS + VWORLD + WFS,WMS

장동규 2020. 11. 11. 16:23

[목표]

제공받은 SHP파일을 DB에 저장하고 웹페이지에서 데이터를 불러와 수정할 수 있는 사이트를 개발

 

[PPT 정리]

drive.google.com/file/d/1rcVbrXGgZy5zdDSkk-uvDB16624rB0cw/view?usp=sharing

 

[환경설정]

*Windows 10 64bit

*Tomcat 9.0

*Java OpenJDK 1.8.0

 

[설치프로그램]

*postgreSQL

www.postgresql.org/

 

PostgreSQL: Downloads

Downloads PostgreSQL Downloads PostgreSQL is available for download as ready-to-use packages or installers for various platforms, as well as a source code archive if you want to build it yourself. Packages and Installers Select your operating system family

www.postgresql.org

설치가이드 : www.slideshare.net/ybh0616/postgis-101511460

 

PostGIS 시작하기

[1] 윈도우에서 PostgreSQL과 PostGIS 설치하기 [2] PostgreSQL/PostGIS에서 DB 생성하고 공간데이터 추가하기 [3] QGIS에서 PostGIS 레이어 추가하기 [4] PostgreSQL/PostGIS에서 SQL 언어 학습하기 [5] PostgreSQL/…

www.slideshare.net

 

*geoServer ( stable )

geoserver.org/

 

Download - GeoServer

Maintenance Long term support, so you have time to upgrade. GeoServer 2.16 releases: Nightly builds for the 2.17.x series can be found here.

geoserver.org

 

war파일 Download

다운로드 파일에서 .war파일만 ./Tomcat/webapps/ 경로에 넣어주고 서버를 재시작합니다.

 

*Openlayers ( cdn 가능 )

openlayers.org/

 

OpenLayers - Welcome

A high-performance, feature-packed library for all your mapping needs.

openlayers.org

 

설치 및 셋팅 이슈사항

vworld 인증키 발급

www.vworld.kr/v4po_main.do

 

공간정보 오픈플랫폼 포털

커뮤니티 지역에 대한 정보습득과 정보공유를 목적으로 하며, 지도기반의 다양한 정보를 효과적으로 공유 할 수 있습니다.

www.vworld.kr

 

CORS문제 해결

https://cafe.naver.com/gisapplication/988 

 

[GeoServer] WFS 이용시 CORS 크로스 도메인

[GeoServer활용] WFS쿼리시 크로스도메인(cross-domain-policy) 해결하기먼저 WFS 및 WMS 서비스는일반적으로 지리 데이터에 액세스하기위한 OGC 웹...

cafe.naver.com

 

libsqlite3-0 not found 시 

libsql3-0.dll Download로드 후 C:\Program Files\PostgreSQL\13\bin\postgisgui에 넣기

http://internetaccessmonitor.ru/dll/libsqlite3-0.dll


[Shp 파일 PostGIS에 등록]

예제 SHP 파일

data.seoul.go.kr/dataList/OA-13221/S/1/datasetView.do

 

서울시 법정구역 읍면동 공간정보 (좌표계: WGS1984)

데이터 이용하기-서울시 법정구역 읍면동 공간정보 (좌표계: WGS1984)

data.seoul.go.kr

미리보기 > MAP > SHP.zip Download

SHP파일을 서버에 등록

1, 2, 3 순서대로 실행
1. DB서버정보 입력
2. Shp 파일 등록 

3. Import 실행

 

pgadmin4 실행
tc_spbe17_2015_w SQL 확인
geometry 컬럼이 wfs geometry정보를 넣게될 컬럼, 괄호안의 값은 Draw할때 도형의 속성


[GeoServer - PostGIS 연결]

 

[geoserver 주소] : 톰캣 주소/geoserver , 도메인/geoserver/web/

 

처음 계정 : admin/geoserver

기본 작업공간, 저장소, 레이어 전부삭제

 

[GeoServer 설정]

작업공간 > 새로운 작업공간 추가하기

 

저장소 > 새로운 저장소 생성하기
레이어 > 새로운레이어 추가

 

*서비스의 모든탭 사용

 

보안 > 데이터 > 새로운 룰 추가하기

 

레이어 미리보기로 확인

확인 완료

 


[Openlayers 연동]

1. WMS

- 이미지로 띄움

- 수정불가

- 속도 빠름, DB정보 표출가능

 

[wms.html]

<!DOCTYPE html>
<html lang="ko">
<head>
	<meta charset="utf-8">
	<meta content="IE=edge" http-equiv="X-UA-Compatible">
	<title>환경주제도 맵 예제(wms)</title>
	<link rel="stylesheet" type="text/css" href="/resources/ol.css">
	<link rel="stylesheet" type="text/css" href="/resources/wms.css">
	<script src="/resources/ol.js"></script>
</head>
<body>
	<!-- Pointer events polyfill for old browsers, see https://caniuse.com/#feat=pointer -->
	<div id="map" class="map"></div>
	<div id="popup" class="ol-popup">
		<a href="#" id="popup-closer" class="ol-popup-closer"></a>
		<div id="popup-content"></div>
	</div>
	<div id = "info"> </div>
	<script src="/resources/wms.js"></script>
</body>
</html>

[wms.css]

@charset "UTF-8";
<style>
	html, body, #map { margin: 0; padding: 0; width: 100%; height: 100%; }
	.map {
		width: 100%;
		height:400px;
	}
	.ol-popup {
		position: absolute;
		background-color: white;
		box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
		padding: 15px;
		border-radius: 10px;
		border: 1px solid #cccccc;
		bottom: 12px;
		left: -50px;
		min-width: 280px;
	}
	
	.ol-popup:after, .ol-popup:before {
		top: 100%;
		border: solid transparent;
		content: " ";
		height: 0;
		width: 0;
		position: absolute;
		pointer-events: none;
	}
	
	.ol-popup:after {
		border-top-color: white;
		border-width: 10px;
		left: 48px;
		margin-left: -10px;
	}
	
	.ol-popup:before {
		border-top-color: #cccccc;
		border-width: 11px;
		left: 48px;
		margin-left: -11px;
	}
	
	.ol-popup-closer {
		text-decoration: none;
		position: absolute;
		top: 2px;
		right: 8px;
	}
	
	.ol-popup-closer:after {
		content: "✖";
	}
</style>

 

[wms.js]

/**
 * Elements that make up the popup.
 */
var container1 = document.getElementById('popup');
var content1 = document.getElementById('popup-content');
var closer1 = document.getElementById('popup-closer');

/**
 * Create an overlay to anchor the popup to the map.
 */
var overlay = new ol.Overlay({
	element : container1,
	autoPan : true,
	autoPanAnimation : {
		duration : 250,
	},
});

/**
 * Add a click handler to hide the popup.
 * @return {boolean} Don't follow the href.
 */
closer1.onclick = function () {
	overlay.setPosition(undefined);
	closer1.blur();
	return false;
};

var wmsSource = new ol.source.TileWMS({
    url: '/geoserver/seoul/wms',
    params: {
  	  'layers':'seoul:tc_spbe17_2015_w',
  	  'TILED':true},
    serverType: 'geoserver',
    // Countries have transparency, so do not fade tiles:
    transition: 0,
    projection: 'EPSG:4326',
});

var layers = [
	new ol.layer.Tile({
		source : new ol.source.XYZ({
			// Vworld Tile 변경
			url : 'http://api.vworld.kr/req/wmts/1.0.0/[인증키]/Satellite/{z}/{y}/{x}.jpeg'
		})
	}),
	new ol.layer.Tile({
		source : wmsSource,
	}) 
];

var view = new ol.View({
	center: [14128579.82, 4512570.74],
	zoom: 11
}); 

var map = new ol.Map({
  layers: layers,
  overlays: [overlay],
  target: 'map',
  view: view,
});


map.on('singleclick', function (evt) {
	var viewResolution = /** @type {number} */(view.getResolution());
	var url = wmsSource.getGetFeatureInfoUrl(evt.coordinate, viewResolution,
			'EPSG:3857', {
				'INFO_FORMAT' : 'text/html',
				'QUERY_LAYERS' : 'tc_spbe17_2015_w'
			});
	
	if (url) {
		fetch(url).then(function(response) {
			return response.text();
		}).then(function(html) {
			document.getElementById('info').innerHTML = html;
			content1.innerHTML = html;
			overlay.setPosition(evt.coordinate);
		});
	}
});

 


[Openlayers 연동2]

2. WFS

- Vector로 띄움

- 수정가능

- 속도 느림

 

[wfs.html]

<!DOCTYPE html>
<html lang="ko">
<head>
	<meta charset="utf-8">
	<meta content="IE=edge" http-equiv="X-UA-Compatible">
	<title>공간지도 서비스(wfs)</title>
	<style type="text/css">
		html, body, #map { margin: 0; padding: 0; width: 100%; height: 100%; }
	</style>
	<link rel="stylesheet" type="text/css" href="/resources/ol.css">
	<link rel="stylesheet" type="text/css" href="/resources/wfs.css">
	<script src="/resources/ol.js"></script>
</head>
<body>
	<!-- Pointer events polyfill for old browsers, see https://caniuse.com/#feat=pointer -->
	<div id="map" class="map"></div>
	<form id="options-form" autocomplete="off">
		<div class="radio">
			<label>Mode  &nbsp;</label> 
			<label> <input type="radio" name="interaction" value="draw" id="rdo_draw" > 
				Draw &nbsp;
			</label>
			
			<label> <input type="radio" name="interaction" value="modify" id="rdo_modify">
				Modify &nbsp;
			</label>
			
			<label> <input type="radio" name="interaction" value="delete" id="rdo_delete">
				Delete &nbsp;
			</label>
			<button type="button" onClick="deleteItem()">삭제</button>
		</div>
		<div class="radio">
		</div>
		<div class="form-group">
			<label>Draw type &nbsp;</label> 
			<select name="draw-type" id="draw-type">
				<option value="Point">Point</option>
				<option value="LineString">LineString</option>
				<option value="Polygon">Polygon</option>
				<option value="Circle">Circle</option>
			</select>
		</div>
		<div class="form-group">
			<label>Map type &nbsp;</label> 
			<select name="map-type" id="map-type">
				<option value="tc_spbe17_2015_w">tc_spbe17_2015_w</option>
                <!-- map 추가 -->
			</select>
		</div>
	</form>
			
	<script src="/resources/wfs.js"></script>
</body>
</html>

 

[wfs.css]

@charset "UTF-8";
<style>
	.map {
		width: 100%;
		height:400px;
	}
</style>

 

[wfs.js]

/* ServerSide Value */
var formatWFS = new ol.format.WFS();

var xs = new XMLSerializer();

//맵생성 시 추가
var gmlJson = {'tc_spbe17_2015_w':{}};
var mapJson = {};
var target_map='tc_spbe17_2015_w';

/* GML 데이터 */
//맵생성시 추가
gmlJson['tc_spbe17_2015_w']['insert'] = new ol.format.GML({
	featureNS : 'seoul',
	featureType : 'tc_spbe17_2015_w',
	srsName: 'EPSG:3857'
});

gmlJson['tc_spbe17_2015_w']['upddel'] = new ol.format.GML({
	featureNS : 'http://localhost/geoserver/seoul/ows',
	featureType : 'tc_spbe17_2015_w',
	srsName: 'EPSG:3857'
});

function transactWFS(mode, f) {
	var node;
	switch (mode) {
	case 'insert':
		node = formatWFS.writeTransaction([ f ], null, null, gmlJson[target_map]['insert']);
		break;
	case 'update':
		node = formatWFS.writeTransaction(null, [ f ], null, gmlJson[target_map]['upddel']);
		break;
	case 'delete':
		node = formatWFS.writeTransaction(null, null, [ f ], gmlJson[target_map]['upddel']);
		break;
	}
	var payload = xs.serializeToString(node);
	switch (mode) {
	case 'update':
		var payload=payload.replace("feature:"+target_map,"seoul:"+target_map);
		break;
	}
	$.ajax('http://localhost/geoserver/seoul/ows', {
		type : 'POST',
		dataType : 'xml',
		processData : false,
		contentType : 'text/xml',
		data : payload
	}).done(function(data1,data2,data3) {
		console.log(data1);
		console.log(data2);
		//mapJson[target_map].clear();
	}).always(function(){
		switch (mode) {
		case 'insert':
			var propNodes = node.getElementsByTagName("Property");
			for (var i = 0; i < propNodes.length; i++) {
				var propNode = propNodes[i];
				var propNameNode = propNode.firstElementChild;
				var propNameNodeValue = propNameNode.firstChild;
				if (propNameNodeValue.nodeValue === "geometry") {
					propNode.parentNode.removeChild(propNode);
					break;
				}
			}
			break;
		}
	});
};

/* 맵데이터 */ 
temp_tc_spbe17_2015_w = new ol.source.Vector({
			loader: function (extent) {
				$.ajax('http://localhost/geoserver/seoul/ows', {
					type: 'GET',
					data: {
						service: 'WFS',
						version: '1.1.0',
						request: 'GetFeature',
						typename: 'seoul:tc_spbe17_2015_w',
						srsname: 'EPSG:3857',
						bbox: extent.join(',') + ',EPSG:3857'
					}
				}).done(function (response) {
					temp_tc_spbe17_2015_w.addFeatures(formatWFS.readFeatures(response));
				});
			},
			strategy: ol.loadingstrategy.bbox,
			projection: 'EPSG:3857'
		});

mapJson['tc_spbe17_2015_w'] = new ol.layer.Vector({
	source : temp_tc_spbe17_2015_w,
	style : new ol.style.Style({
		fill : new ol.style.Fill({
			color : 'rgba(255, 0, 0, 0.2)',
		}),
		stroke : new ol.style.Stroke({
			color : '#ff0000',
			width : 2,
		}),
		image : new ol.style.Circle({
			radius : 7,
			fill : new ol.style.Fill({
				color : '#ff0000',
			}),
		}),
	}),
});

var map = new ol.Map({
	layers : [ 
		new ol.layer.Tile({
			source : new ol.source.XYZ({
				url : 'http://api.vworld.kr/req/wmts/1.0.0/[인증키]/Satellite/{z}/{y}/{x}.jpeg'
			})
		}), 
		mapJson['tc_spbe17_2015_w'] 
	],
	target : document.getElementById('map'),
	view : new ol.View({
		center : [ 14128579.82, 4512570.74 ],
		maxZoom : 19,
		zoom : 11
	})
});

/* Delete */
var ExampleDelete = {	
	init : function() {
		this.select = new ol.interaction.Select();
		map.addInteraction(this.select);

		this.setEvents();
		this.setActive(false);
		
		this.selectItem = null;
	},
	setEvents : function() {
		var selectedFeatures = this.select.getFeatures();
		selectedFeatures.on('add', function(event){
			this.selectItem = event.element;
		}, this);
		selectedFeatures.on('remove', function(event){
			this.selectItem = null;
		}, this);
	},
	setActive : function(active) {
		this.select.setActive(active);
	},
};
ExampleDelete.init();

function deleteItem(){
	if( rdo_delete.checked == true ){
		if( ExampleDelete.selectItem != null ){
			if (confirm("정말 삭제하시겠습니까?")) {
				transactWFS('delete', ExampleDelete.selectItem);
				mapJson[target_map].getSource().removeFeature(ExampleDelete.selectItem);
				ExampleDelete.select.getFeatures().clear();
			}
		}	
	}
}
/* ModiFy */
var ExampleModify = {
	init : function() {
		this.select = new ol.interaction.Select();
		map.addInteraction(this.select);

		this.modify = new ol.interaction.Modify({
			features : this.select.getFeatures(),
		});
		map.addInteraction(this.modify);

		this.setEvents();
		this.setActive(false);
	},
	setEvents : function() {
		var selectedFeatures = this.select.getFeatures();

		this.select.on('change:active', function() {
			selectedFeatures.forEach(function(each) {
				selectedFeatures.remove(each);
			});
		});
		
		this.dirty = {};
		
		this.select.getFeatures().on('add', function (e) {
			e.element.on('change', function (e) {
				ExampleModify.dirty[e.target.getId()] = true;
			});
		});
		
		this.select.getFeatures().on('remove', function (e) {
			var f = e.element;
			if (ExampleModify.dirty[f.getId()]) {
				delete ExampleModify.dirty[f.getId()];
				var featureProperties = f.getProperties();
				delete featureProperties.boundedBy;
				var clone = new ol.Feature(featureProperties);
				clone.setId(f.getId());
				transactWFS('update', clone);
			}
		});
	},
	setActive : function(active) {
		this.select.setActive(active);
		this.modify.setActive(active);
	},
};
ExampleModify.init();

/* Draw */
var optionsForm = document.getElementById('options-form');
var drawObj = null;
var ExampleDraw = {
	init : function() {
		this.setDrawType();
		map.addInteraction(this.Point);
		map.addInteraction(this.LineString);
		map.addInteraction(this.Polygon);
		map.addInteraction(this.Circle);

		this.Point.on('drawend',function(e){
			transactWFS('insert', e.feature);
		})
		this.LineString.on('drawend',function(e){
			transactWFS('insert', e.feature);
		})
		this.Polygon.on('drawend',function(e){
			var f = e.feature;
			f.set('emd_cd', '11111111');
			f.set('emd_nm', '테스트');
			f.set('emd_eng_nm', 'test');
			f.setGeometryName('geom');
			drawObj = f;
			transactWFS('insert', e.feature);
		})
		this.Circle.on('drawend',function(e){
			transactWFS('insert', e.feature);
		})
		
		this.Point.setActive(false);
		this.LineString.setActive(false);
		this.Polygon.setActive(false);
		this.Circle.setActive(false);
		// The snap interaction must be added after the Modify and Draw interactions
		// in order for its map browser event handlers to be fired first. Its handlers
		// are responsible of doing the snapping.
		map.addInteraction(this.snap);
	},
	setDrawType:function(){
		var source = mapJson[target_map].getSource(); 
		this.Point = new ol.interaction.Draw({
			source : source,
			type : 'Point',
			geometryName : 'geom',
		});
		this.LineString = new ol.interaction.Draw({
			source : source,
			type : 'MultiLineString',
			geometryName : 'geom',
		});
		this.Polygon = new ol.interaction.Draw({
			source : source,
			type : 'MultiPolygon',
			geometryName : 'geom',
		});
		this.Circle = new ol.interaction.Draw({
			source : source,
			type : 'Circle',
			geometryName : 'geom',
		});
		this.snap = new ol.interaction.Snap({
			source: source,
		});
	},
	getActive : function() {
		return this.activeType ? this[this.activeType].getActive() : false;
	},
	setActive : function(active) {
		var type = optionsForm.elements['draw-type'].value;
		if (active) {
			this.activeType && this[this.activeType].setActive(false);
			this[type].setActive(true);
			this.activeType = type;
		} else {
			this.activeType && this[this.activeType].setActive(false);
			this.activeType = null;
		}
	},
	removeInteraction: function(){
		map.removeInteraction(this.Point);
		map.removeInteraction(this.LineString);
		map.removeInteraction(this.Polygon);
		map.removeInteraction(this.Circle);
		map.removeInteraction(this.snap);
	}
};
ExampleDraw.init();

/**
 * Let user change the geometry type.
 * @param {Event} e Change event.
 */

optionsForm.onchange = function (e) {
	var type = e.target.getAttribute('name');
	var value = e.target.value;
	if (type == 'draw-type') {
		ExampleDraw.getActive() && ExampleDraw.setActive(true);
	}else if(type == 'map-type'){
		if(rdo_draw.checked){
			rdo_draw.checked = false;
		}else if(rdo_modify.checked){
			ExampleModify.setActive(false);
			ExampleModify.setActive(true);
		}else if(rdo_delete.checked){
			ExampleDelete.setActive(false);
			ExampleDelete.setActive(true);
		}
		map.removeLayer(mapJson[target_map]);
		map.addLayer(mapJson[value]);
		ExampleDraw.removeInteraction();
		
		target_map = value;
		ExampleDraw.init();
	} else if (type == 'interaction') {
		if (value == 'modify') {
			ExampleDraw.setActive(false);
			ExampleModify.setActive(true);
			ExampleDelete.setActive(false);
		} else if (value == 'draw') {
			ExampleDraw.setActive(true);
			ExampleModify.setActive(false);
			ExampleDelete.setActive(false);
		} else if (value == 'delete') {
			ExampleDraw.setActive(false);
			ExampleModify.setActive(false);
			ExampleDelete.setActive(true);
		}
	}
};

 

wms화면

 

wfs 화면

 

Draw : Drawend 이벤트를 통하여 자동 반영 ( 이때 위에 postgis의 DB 테이블에서 geometery 컬럼을 확인하여 컬럼이름과 Geometry 도형중 어떤도형이 들어가는지 확인하여 그 도형만 그릴것, 예제는 MultiPolygon )

Update : remove 이벤트를 통하여 자동 반영

Delete : Prompt 호출 후 응답여부에 따라 저장