logo

𝝅번째 알파카의 개발 낙서장

OpenLayers를 여행하는 개발자를 위한 안내서 - 19. WMS에 팝업 붙이기

프로젝트
⏰ 2022-05-28 11:55:23

D O W N

https://user-images.githubusercontent.com/50317129/156607880-c5abad92-1991-4c01-b85f-7153bf89cb64.png
OpenLayers를 여행하는 개발자를 위한 안내서
이 게시글은 OpenLayers를 여행하는 개발자를 위한 안내서 시리즈의 26개 중 19번 째 게시글입니다.
https://user-images.githubusercontent.com/50317129/260317030-e4b8575b-f09e-47f4-ab70-168a817268c6.png

Table of Contents

https://user-images.githubusercontent.com/50317129/260317030-e4b8575b-f09e-47f4-ab70-168a817268c6.png

개요

WFS와 같은 객체 기반의 지도가 아닌, WMS와 같이 이미지 기반의 지도에도 팝업을 표현할 수 있을까?

표면적으론 불가능해보인다. WFS의 경우 스크립트 상에서 공간정보를 갖고 있으므로, 이를 적절히 활용하면 원하는 정보를 보여줄 수 있었다. 하지만 WMS의 경우 기반 자체가 이미지이므로, 분석 가능한 데이터로써의 활용성은 매우 떨어진다.

즉, 컴퓨터가 이 한 장의 이미지에서 어떤 객체를 얼마나 갖고 있는지 직접적으로 알 수 없다.

그렇다면 WMS 지도는 팝업을 표현하는 게 불가능한 걸까?




GetFeatureInfo

이 때 고려해볼만한 것이 이 GetFeatureInfo라는 프로토콜이다. GetFeatureInfo는 현재 지도의 BBOX, 클릭한 좌표, 지도 상의 픽셀값을 통해 계산된 위치에 있는 Feature들의 정보를 제공해준다.

즉, 지도 이미지를 클릭하는 것으로 Feature의 존재 여부와 Feature의 정보에 대해 캐낼 수 있다. 이를 통해 팝업을 구현할 수 있을 것이다.


🔗 1장에서 다룬 WMS 지도를 그대로 사용한다.

OpenLayers는 Overlay 객체를 통해 지도 위에 원하는 HTML 태그를 띄울 수 있다. OpenLayers가 동작을 담당하긴 하지만, canvas 위에서 렌더링되는 객체는 아니고, 실제 DOM을 출력시켜준다.

Overlay는 기본적으로 지도 상에서 클릭한 위치를 따라간다. 즉, 지도의 특정 위치에서 Overlay를 띄우고 지도를 움직이면, 위치가 고정되지 않고 마커를 따라간다. 이런 동작을 실제 DOM에서 관리하려면 매우 귀찮은 핸들링이 필요할 것이다. 이러한 특징 덕분에 팝업과 매우 잘 어울리는 객체다.



1. GetFeatureInfo URL 구성하기

TXT

1
GET https://example.com/geoserver/wms?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&FORMAT=image%2Fpng&TRANSPARENT=true&QUERY_LAYERS=test:building&layers=buld_sejong&exceptions=application%2Fjson&INFO_FORMAT=application%2Fjson&I=221&J=178&WIDTH=256&HEIGHT=256&CRS=EPSG%3A3857&STYLES=&BBOX=14169590.555392835%2C4366694.551875548%2C14169896.303505976%2C4367000.299988689
ParameterExampleRequireDescription
serviceWMS (고정)Y서비스명
version1.3.0 (고정), 1.1.1, 1.1.0, 1.0.0Y버전
requestGetFeatureInfo (고정)Y요청명
layersrepo_name:layer_nameY레이어명 (다수는 쉼표로 구분)
stylesstyle1적용할 스타일명 (GetFeatureInfo에선 의미 없음)
crs(or srs)EPSG:4326기준 좌표계 (비울 경우 레이어의 기본 좌표계로 인식)
bboxxmin,ymin,xmax,ymaxx_{min},y_{min},x_{max},y_{max}Y이미지 영역 좌표
width256Y이미지 넓이
height256Y이미지 높이
query_layersrepo_name:layer_nameY추가 요청 레이어명 (다수는 쉼표로 구분)
info_formatapplication/vnd.ogc.se_xml (기본)응답 형식
feature_count1 (기본)최대 객체 호출 수
x(or i)225Y지도의 x 픽셀값
y(or j)156Y지도의 y 픽셀값
exceptionsapplication/vnd.ogc.se_xml (기본)예외 응답 형식

GetFeatureInfo의 경우도 GetImage와 마찬가지로 입력해야할 파라미터의 갯수가 많다. GetFeatureInfo 자체가 클릭 시 해당 위치의 마커 정보를 반환하는 API이므로, 지도 상의 클릭한 위치를 x, y의 형태로 제공해야한다. 여러모로 까다로운 파라미터들이 많은 편.

다행히 GetImage와 마찬가지로 OpenLayers에서 GetFeatureInfo URL를 생성해주는 객체를 제공해주니, 이를 사용하면 쉽게 호출할 수 있다. 이 방법은 Overlay의 이벤트를 핸들링하는 부분에서 서술한다. 여기선 그냥 순수 URL로 이와 같이 호출할 수 있다는 점만 알아두자.



2. Overlay 생성하기

Overlay 객체를 직접 생성해보자.

ParameterTypeDefaultDescription
idnumber | string | undefined오버레이 아이디
🔗 ol/Map-Map#getOverlayById 메서드 호출 시 사용됨
elementHTMLElement | undefined오버레이 대상 Element
offsetArray<number>[ 0, 0 ]오버레이 출력의 오프셋(px)
[ x, y ]이며, x는 좌우, y는 상하를 의미한다.
각각 값이 커질 수록 우측, 아래로 이동한다.
position🔗 ol/events/condition-Condition | undefined오버레이의 출력 위치
positioning🔗 ol/OverlayPositioningtop-left오버레이 배치 기준
bottom-left, top-right 등의 값을 가진다.
stopEventbooleantrue이벤트 전파 여부
true일 경우 클래스가 ol-overlay container-stopevent인 DOM에 배치
false일 경우 className 속성에 지정된 값을 클래스로 가진 DOM에 배치
insertFirstbooleantrue오버레이를 엘리먼트에 먼저 삽입할 지, 추가할 지 선택
autoPan🔗 ol/Overlay-PanIntoViewOptions | booleanfalse오버레이 setPosition 호출 시, 오버레이가 완전히 보이도록 지도 자동 이동
autoPanAnimation🔗 ol/Overlay-PanOptionsautoPan 활성화로 인한 이동 시 애니메이션 설정.
autoPan 설정이 boolean이 아닌 객체일 경우 해당 설정은 무시됨
autoPanMarginnumber20autoPan 활성화로 인한 이동 시 오버레이와 지도 테두리 사이의 여백
autoPan 설정이 boolean이 아닌 객체일 경우 해당 설정은 무시됨
autoPanOptions🔗 ol/Overlay-PanIntoViewOptions | undefinedautoPan의 옵션
autoPanAnimation, autoPanMargin 보다 우선 시 됨
classNamestringol-overlay-container ol-selectableCSS 클래스 값

생성 방법은 아래와 같다.

HTML

1
<div id="map-popup"></div>

팝업 시 사용할 DOM을 입력한다. DOM을 특정하기 위해 id 혹은 class 속성을 적절히 활용하자. HTML 코드 상 태그의 위치는 크게 상관없다.

TYPESCRIPT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Overlay } from 'ol';

const popup = document.getElementById('map-popup') as HTMLElement | null;

const overlay = new Overlay({
	id: 'popup',
	element: popup || undefined,
	positioning: 'center-center',
	autoPan: {
		animation: {
			duration: 250
		}
	}
});

태그를 할당하고 적절히 설정하면 Overlay 객체를 생성할 수 있다.

Overlay에 대한 전체 정보는 🔗 공식 문서에서 확인할 수 있다.



3. Map에 적용하기

생성한 OverlayMap에 적용시켜본다.

TYPESCRIPT

1
2
3
4
5
6
7
import { Map } from 'ol';

const map = new Map({
	// ...
	overlays: [ overlay ]
	// ...
});

위와 같이 적용이 가능하다. 배열의 형태로 Overlay를 등록할 수 있다.

TYPESCRIPT

1
2
3
4
5
6
7
8
9
10
11
// 오버레이 추가
map.addOverlay();

// 아이디별 오버레이 반환
map.getOverlayById();

// 오버레이 리스트 반환
map.getOverlays();

// 오버레이 제거
map.removeOverlay();

관련 메서드는 위와 같다. 생성된 Map 객체에서 호출 가능하다.



3. Overlay 이벤트 적용하고 GetFeatureInfo 호출하기

여기서부터는 WFS와 WMS의 방식이 좀 다르다. WFS의 경우, GetFeature의 정보를 토대로 스크립트 상에서 해당 정보에 접근하여 Feature의 정보를 바로 보여줄 수 있었다.

하지만 누차 언급하듯이, WMS의 GetImage는 공간정보를 토대로 이미지를 렌더링하여 반환하기 때문에, 직접적으로 Feature의 정보를 보여주는 데 한계가 있다.

클릭 시 GetFeatureInfo를 활용하면 해당 클릭 위치의 Feature 데이터를 받아올 수 있으므로, 이를 활용한다.

TYPESCRIPT

1
2
3
4
5
6
7
8
import ImageLayer from 'ol/layer/Image';

const layer = new TileLayer({
	source: source,
	minZoom: 15,
	properties: { name: 'wms' },
	zIndex: 5
});

WMS 레이어는 위와 같이 선언됐다고 가정한다.

TSX

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
map.on('singleclick', (e) =>
{
	// WMS properties의 name이 wms인 레이어를 추출
	const wmsLayer = map.getAllLayers().filter(layer => layer.get('name') === 'wms')[0];

	// WMS 레이어의 Source 호출
	const source: TileWMS | ImageWMS = wmsLayer.getSource();

	// GetFeatureInfo URL 생성
	const url = source.getFeatureInfoUrl(e.coordinate, map.getView().getResolution() || 0, 'EPSG:3857', {
		QUERY_LAYERS: 'test:building',
		INFO_FORMAT: 'application/json'
	});

	// GetFeatureInfo URL이 유효할 경우
	if (url)
	{
		const request = await fetch(url.toString(), { method: 'GET' }).catch(e => alert(e.message));

		// 응답이 유효할 경우
		if (request)
		{
			// 응답이 정상일 경우
			if (request.ok)
			{
				const json = await request.json();

				// 객체가 하나도 없을 경우
				if (json.features.length === 0)
				{
					overlay.setPosition(undefined);
				}

				// 객체가 있을 경우
				else
				{
					// GeoJSON에서 Feature를 생성
					const feature = new GeoJSON().readFeature(json.features[0]);

					// 생성한 Feature로 VectorSource 생성
					const vector = new VectorSource({ features: [ feature ] });

					setPopupState(
						<ul>
							<li>{feature.getId() || ''}</li>
							<li>{feature.get('buld_nm') || <span>이름 없음</span>}</li>
							<li>{feature.get('bul_man_no')}</li>
						</ul>
					);

					overlay.setPosition(getCenter(vector.getExtent()));
				}
			}

			// 아닐 경우
			else
			{
				alert(request.status);
			}
		}
	}
});

위와 같이 클릭 이벤트를 적절히 활용한다.

  1. 클릭 시, WMS 레이어를 호출한다.
  2. WMS 레이어에서 Source 객체를 호출한다.
  3. Source 객체의 getFeatureInfoUrl 메서드를 통해 GetFeatureInfo URL을 생성한다.
  4. GeoServer에 GetFeatureInfo를 호출한다.
  5. 응답의 GeoJSON을 토대로 Feature를 만들어 VectorSource 객체를 생성한다.
  6. 생성한 Feature 객체에서 원하는 데이터를 받아 호출한다.
  7. vector.getExtent()를 통해 데이터의 실제 위치를 계산하여 Overlay 위치로 지정한다.

위와 같은 방식으로 로직이 진행된다. 물론 어디까지나 사용의 한 예시이므로, 이벤트에 원하는 동작을 기술하여 다양한 동작을 수행할 수 있다.




예제 확인하기

🔗 OpenLayers6 Sandbox - WMS Popup에서 이를 구현한 예제를 확인할 수 있다.

🏷️ Related Tag

# GIS
# GeoServer
# OpenLayers
# WMS

😍 읽어주셔서 감사합니다!
도움이 되셨다면, 💝공감이나 🗨️댓글을 달아주시는 건 어떤가요?
블로그 운영에 큰 힘이 됩니다!
https://blog.itcode.dev/projects/2022/05/28/gis-guide-for-programmer-19