[OpenLayers] ol v5.3을 VSCode에서 사용하기

  1. VSCode(VisualStudio Code)를 설치한다.
  2. node.js를 설치한다.
  3. 콘솔에서 작업 디렉토리를 생성하고 이동한다.
  4. 아래의 명령을 입력하여 필요한 정보 입력(단순히 enter 키를 눌러 기본값으로 지정해도 됨)
  5. npm init
  6. 아래의 명령을 입력하여 openlayers 패키지 설치
  7. npm install ol
  8. 아래의 명령을 입력하여 parcel 패키지 설치(아직 명령창 닫지 말 것)
  9. npm install --save-dev parcel-bundler
  10. VSCode를 실행하고 작업 디렉토리를 오픈한다.
  11. index.js 파일을 추가하고 아래처럼 입력한다.
  12. import 'ol/ol.css';
    import {Map, View} from 'ol';
    import TileLayer from 'ol/layer/Tile';
    import OSM from 'ol/source/OSM';
    
    const map = new Map({
      target: 'map',
      layers: [
        new TileLayer({
          source: new OSM()
        })
      ],
      view: new View({
        center: [0, 0],
        zoom: 0
      })
    });
    
  13. index.html 파일을 추가하고 아래처럼 입력한다.
  14. <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>Using Parcel with OpenLayers</title>
        <style>
          #map {
            width: 400px;
            height: 250px;
          }
        </style>
      </head>
      <body>
        <div id="map"></div>
        <script src="./index.js"></script>
      </body>
    </html>
    
  15. package.json 파일에 아래의 내용을 추가한다.
  16. "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "start": "parcel index.html",
      "build": "parcel build --public-url . index.html"
    }
    
  17. 앞서 열어 둔 명령창에서 다음을 입력한다.
  18. npm start
    
  19. VSCode에서 Debugger for Chrome 확장 기능을 설치한다.
  20. VSCode에서 F5키를 눌러 Start Debugging을 실행하고, 실행 환경으로 Chrome을 선택한다.
  21. .vscode라는 폴더 안에 launch.json 파일을 생성되는데, 이 파일을 연다.
  22. 내용 중 “url”: “http://localhost:8080″를 아래처럼 변경한다.
    "url": "http://localhost:1234"
  23. 내용 중 “webRoot”: “${workspaceFolder}”를 아래처럼 변경한다.
    "webRoot": "${workspaceFolder}/dist"

[OpenLayers3] 가장 가까운 포인트 추적

현재 지도 상에 표시된 원 모양의 포인트 피쳐에 대해 마우스 커서 위치에서 가장 가까운 원을 추적해 효과적으로 시각화 해주는 예제를 만들어 보겠습니다. 실행 결과는 아래와 같으니 한번 마우스를 올려 놓고 요래~요래~ 움직여 보세요~

위와 같은 예제를 만들어 보겠습니다. 먼저 필요한 외부 CSS와 JavaScript 라이브러리를 아래처럼 추가합니다.




UI를 만들어 것인데요. 보면 지도 하나 딸랑~ 있습니다. 이 지도는 div 요소에 표시되므로 아래와 같습니다. 이 div의 id를 map으로 지정해 이름을 붙여 추후 참조할 수 있도록 하였습니다.


    

자, 이제 코드를 작성해 이 UI에 영혼을 불어 넣어 보겠습니다. 수리수리마수리..

jQuery의 기능 중 페이지의 구성 요소가 모두 준비되었을때 호출되는 이벤트를 아래처럼 등록합니다.

<script>
    $(function () {
        ....
    });

앞으로 우리가 작성할 모든 코드는 위에서 …. 부분에 추가되는데요. 먼저 지도 화면에 표시할 원에 대한 피쳐(Feature)를 4000개 생성합니다. 이 피쳐의 지오메트리 타입은 포인트이므로 무작위 위치를 가지도록 ol.geom.Point로 지정하고 i와 size라는 속성값을 지정해 둡니다. size에는 i값에 따라 10 또는 20의 값이 지정되도록 하였습니다.

var count = 4000;
var features = new Array(count);
var e = 18000000;

for (var i = 0; i < count; ++i) {
    features[i] = new ol.Feature({
        'geometry': new ol.geom.Point([2 * e * Math.random() - e, 2 * e * Math.random() - e]),
        'i': i,
        'size': i % 2 ? 10 : 20
    });
}

그리고 이 4000개의 포인트에 대해 어떤 스타일로 표시할지를 지정해야 하는데요. 이때 사용하는 styles 객체를 생성해 둡니다.

var styles = {
    '10': new ol.style.Style({
        image: new ol.style.Circle({
            radius: 5,
            fill: new ol.style.Fill({ color: '#666666' }),
            stroke: new ol.style.Stroke({ color: '#bada55', width: 1 })
        })
    }),
    '20': new ol.style.Style({
        image: new ol.style.Circle({
            radius: 10,
            fill: new ol.style.Fill({ color: '#666666' }),
            stroke: new ol.style.Stroke({ color: '#bada55', width: 1 })
        })
    })
};

위의 코드를 보면 10과 20에 대한 원형 스타일 2개를 준비하고 있습니다. 이는 이후의 코드에서 살펴보겠지만, 피쳐의 size 속성에 따라 스타일을 결정하게 됩니다.

ol3에서는 지도 상에 표시되는 것은 레이어(layer)라는 개념이 필요합니다. 이 예제의 경우, 좀더 정확하게는 ol.layer.Vector 레이어 타입인데요. Vector는 도형에 대한 형상을 표현하기 위한 레이어입니다. 레이어는 자신이 표현해야할 데이터를 제공해 주는 데이터소스가 필요합니다. 이 데이터 소스에 대한 객체를 아래처럼 추가합니다.

var vectorSource = new ol.source.Vector({
    features: features,
    wrapX: false
});

위의 코드를 보면 4000개의 포인트 피쳐를 담고 있는 features를 통해 ol.source.Vector 타입의 데이터소스 객체를 생성하고 있습니다. 이제 ol.layer.Vector 레이어를 위한 모든 것이 준비되었으니 레이어를 생성합니다.

var vector = new ol.layer.Vector({
    source: vectorSource,
    style: function (feature) {
        return styles[feature.get('size')];
    }
});

앞서 만들어 두었던 데이터소스를 지정하고 있구요. style 속성에 함수를 지정해 표시해야할 피쳐에 대한 size 속성값에 따라 스타일을 결정하도록 하였습니다.

이제 이렇게 생성된 레이어를 지도 객체에 지정합니다.

var map = new ol.Map({
    layers: [
        new ol.layer.Tile({ source: new ol.source.OSM() }),
        vector
    ],
    target: document.getElementById('map'),
    view: new ol.View({
        center: [0, 0],
        zoom: 6
    })
});

위의 코드 중 2번 줄에 Open Street Map와 앞서 생성한 벡터 레이어를 구성 레이어들로 지정하고 있습니다. 6번은 지도를 표현할 div 요소를 지정하는 것이고, 7번은 지도의 초기 중심 좌표와 줌 레벨을 지정하고 있습니다.

지금까지의 지도 객체에 벡터 레이어를 지정하기 위해 생성한 객체들의 관계를 클래스 다이어그램으로 표현하면 아래와 같습니다.

자, 이제 마우스 커서의 위치에서 가장 가까운 원을 추적할 모든 준비가 끝났습니다. 가장 가까운 원을 추적할때 추적된 원과 커서 사이에 피드백을 시각화할 것인데요. 이를 위해 아래처럼 2개의 전역 변수와 1개의 함수가 필요합니다.

var point = null;
var line = null;

var displaySnap = function (coordinate) {
    var closestFeature = vectorSource.getClosestFeatureToCoordinate(coordinate);

    if (closestFeature === null) {
        point = null;
        line = null;
    } else {
        var geometry = closestFeature.getGeometry();
        var closestPoint = geometry.getClosestPoint(coordinate);

        if (point === null) {
            point = new ol.geom.Point(closestPoint);
        } else {
            point.setCoordinates(closestPoint);
        }

        if (line === null) {
            line = new ol.geom.LineString([coordinate, closestPoint]);
        } else {
            line.setCoordinates([coordinate, closestPoint]);
        }
    }

    map.render();
};

displaySnap이라는 함수를 설명하면, 함수의 인자로 마우스 커서 위치에 대한 지도 좌표를 받습니다. 5번 코드에서 앞서 생성해 두었던 벡터 레이어의 데이터소스의 getCloestFeatureToCoordinate라는 매우 설명에 충실한 함수를 통해서 데이터 소스의 피쳐들 중 현재 마우스 커서 위치에서 가장 가까운 피쳐를 검사해 존재한다면 반환합니다. 만약 7번의 if 문을 통해 가까운 피쳐가 존재하지 않는다면 point와 line 변수를 null로 지정하고, 가까운 피쳐가 존재한다면 point 변수는 가장 가까운 포인트 피쳐의 위치를 가지는 ol.geom.Point 객체를 생성해 지정하고 line에는 커서의 지도 좌표 및 가장 가까운 포인트 피쳐의 위치 좌표로 구성된 ol.geom.LineString 객체를 생성해 지정합니다. 이 point와 line을 지도 상에 그려주면 피드백 시각화가 완성되는 것입니다. 이러한 시각화를 위해 일단 28번 코드에서 map.render() 함수를 호출하는데요. 이 함수가 호출되면 map에서 postcompose 이벤트가 호출되므로 이 이벤트에 대한 코드를 살펴보면..

var stroke = new ol.style.Stroke({
    color: 'rgba(255,255,0,0.9)',
    width: 3
});

var style = new ol.style.Style({
    stroke: stroke,
    image: new ol.style.Circle({
        radius: 10,
        stroke: stroke
    })
});

map.on('postcompose', function (evt) {
    var vectorContext = evt.vectorContext; // ol.render.VectorContext 
    vectorContext.setStyle(style);

    if (point !== null) {
        vectorContext.drawGeometry(point);
    }

    if (line !== null) {
        vectorContext.drawGeometry(line);
    }
});

위의 코드를 살펴보면, 1번과 6번의 코드에서 앞서 point 객체와 line 객체를 그리기 위해 사용되는 스타일 객체를 생성해 둡니다. 그리고 14번에서 map에 대해 postcompose 이벤트를 등록하는데요. 보시면 앞서 생생해둔 point, line을 drawGeometry 함수를 이용해 그려주고 있습니다.

자, 이제 마지막으로 displaySnap 함수를 적정한 시점에서 호출해 주기만 하면 되는데요. 그 시점은 지도 상에 마우스 커스를 이동하거나 클릭할때 정도가 될것 같습니다. 이 마우스 이동과 클릭에 대한 이벤트 코드를 아래처럼 지정해 둡니다.

map.on('pointermove', function (evt) {
    if (evt.dragging) {
        return;
    }

    var coordinate = map.getEventCoordinate(evt.originalEvent);
    displaySnap(coordinate);
});

map.on('click', function (evt) {
    displaySnap(evt.coordinate);
});

이상으로 지금까지 작성된 코드는 아래의 링크를 통해 다운로드 받을 수 있습니다.

[OpenLayers3] 레이어 스파이(Layer Spy)

아래의 지도 위에 마우스를 움직여 보기 바랍니다.


네! 바로 이 기능이 ‘레이어 스파이(Layer Spy)’입니다. 이 기능을 OpenLayer3에서 구현해 보겠습니다.

먼저 필요한 외부 css 및 js를 페이지에 추가합니다.

<script src="https://openlayers.org/en/v3.19.1/build/ol.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>

UI를 구성할 텐데요. 위의 지도 페이지에는 지도에 대한 DIV 요소만이 있습니다. 아래의 코드처럼 말입니다.

이제 필요한 css 및 외부 js 라이브러리와 함께 UI까지 준비가 되었으니, 스크립트 코드를 작성할 차례입니다. jQuery의 이벤트 중 UI 등과 같은 요소의 준비가 완료되면 호출되는 ready 이벤트를 아래처럼 작성해 둡니다.

$(function() {
     ....
});

위의 코드에서 …에 해당하는 부분에 스크립트 코드를 추가할 것입니다.

먼저 지도를 구성하는 레이어로 2개를 준비할텐데요. Bing Map에서 제공하는 타일맵을 통해 레이어를 구성합니다. Bing Map의 지도 서비스를 사용하기 위해서는 key가 필요한데, 만약 아래의 코드에서 제공하는 key로 지도가 표시되지 않는다면 직접 발급을 받아 적용하시기 바라니다. 코드는 아래와 같습니다.

var key = 'AgRcrlx0phSk0kDN6HFX9HkMLbG3dBIqtTm-2no4igx0xJPXeDTffQAQfQP1e-Xv';

var roads = new ol.layer.Tile({
    source: new ol.source.BingMaps({ key: key, imagerySet: 'Road' })
});

var imagery = new ol.layer.Tile({
    source: new ol.source.BingMaps({ key: key, imagerySet: 'Aerial' })
});

이제 이 레이어를 지도에 추가하고 지도를 표시할 DIV 요소에 적용할 차례입니다. 아래의 코드와 같습니다.

var container = document.getElementById('map');

var map = new ol.Map({
    layers: [roads, imagery],
    target: container,
    view: new ol.View({
        center: ol.proj.fromLonLat([-109, 46.5]),
        zoom: 6
    })
});

먼저 1번 코드는 지도를 표시할 DIV에 대한 DOM 요소를 id값을 통해 가져와 container이라는 변수에 할당합니다. 그리고 3번은 지도 객체를 생성하는데요. 생성 시 옵션값으로 layers, target, view를 설정합니다. layers 옵션값은 앞서 생성해 둔 레이어를 추가하고, target은 지도가 표시될 DIV에 대한 DIM 요소가 지정되며, view 옵션에는 처음 지도의 위치와 줌 레벨을 지정합니다. layers 옵션에 대해 추가적으로 설명하면.. roads 레이어를 먼저 추가하고 다음에 imagery 레이어를 추가하고 있다는 점이 중요한데요. 이렇게 하면 roads 레이어 위에 imagery 레이어가 표시되는데, 이때 위체 표시되는 imagery 레이어에 대한 렌더링 컨텍스트에 대해 자르기(Clipping)을 적용하면 레이어 스파이 기능이 완성됩니다.

imagery 레이어에 대한 자르기(Clipping)의 형태는 원 모양입니다. 이 원을 위한 반경값으로 사용할 radius 변수와 키보드를 통해 이 radius 변수를 증가하고 감소하는 키 입력 이벤트를 지정합니다. 코드는 아래와 같습니다.

var radius = 100;

document.addEventListener('keydown', function (evt) {
    if (evt.which === 38) { // Up Key
        radius = Math.min(radius + 5, 200);
        map.render();
        evt.preventDefault();
    } else if (evt.which === 40) { // Down Key
        radius = Math.max(radius - 5, 25);
        map.render();
        evt.preventDefault();
    }
});

위의 코드에서, radius 변수값을 증가시키고 감소시키는 키를 입력할때마다 map 객체에 대한 render함수가 호출되고 있습니다. 이 render 함수는 다음과 같은 실행 흐름을 갖습니다.

먼저 precompose 이벤트가 발생하는데, 이 이벤트는 레이어를 그리기(Drawing) 직전에 발생합니다. 그 다음은 실제로 레이어 그리기(Layer Drawing)가 실행됩니다. 레이어 그리기가 완료되면 postcompose 이벤트가 호출됩니다. 이러한 3 단계는 각 레이어마다 반복됩니다. 또 여기서 중요한점은 이 3 단계가 실행되기에 앞서 지도 데이터를 서버측에 요청하여 받게 되는 것이지 이 3 단계 안에서 서버측으로부터 지도 데이터를 받는 것이 아닙니다. 이 3단계에서 imagery 레이어를 원 모양으로 자르기 위해서 주목해야할 이벤트는 레이어가 그려지기 전인 precompose 이벤트입니다. 아래는 imagery 레이어에(지도가 아닌) precompose 이벤트를 지정하는 코드입니다.

imagery.on('precompose', function (event) {
    var ctx = event.context;
    var pixelRatio = event.frameState.pixelRatio;
    ctx.save();
    ctx.beginPath();
    
    if (mousePosition) {
        ctx.arc(mousePosition[0] * pixelRatio, mousePosition[1] * pixelRatio,
            radius * pixelRatio, 0, 2 * Math.PI);
        ctx.lineWidth = 5 * pixelRatio;
        ctx.strokeStyle = 'rgba(255,255,0,1)';
        ctx.stroke();
    }
    
    ctx.clip();
});

위의 코드를 살펴보면, 2번에서 레이어를 그릴 컨텍스트를 얻어와 ctx 변수에 저장합니다. 그리고 3번에서는 현재 지도에 대한 픽셀 비율값을 가져와 pixelRatio 변수에 저장하고 있습니다. 4번ㅡ15번의 코드가 원 모양으로 자르기(Clipping) 하는 코드입니다. 코드를 부면 mousePosition 변수가 보이는데요. 이 변수는 마우스를 지도 위에서 움직일때 현재 마우스 커서의 좌표를 담게 됩니다. 이를 위해서 마우스 이벤트 지정이 필요한데요. 아래의 코드와 같습니다. 잘라내기(Clipping)의 모양을 지정하기 위해 컨텍스의 arc 함수를 호출하고 있는데요. 중심 좌표 및 반경에 대한 인자값 모두에 pixelRatio 값을 곱해주고 있습니다. 이 pixelRatio는 모니터 화면에 대한 물리 픽셀(physical Pixels)와 장치독립적인 픽셀(device-independent pixels)의 비(ratio) 값입니다.

var mousePosition = null;

container.addEventListener('mousemove', function (event) {
    mousePosition = map.getEventPixel(event);
    map.render();
});

container.addEventListener('mouseout', function () {
    mousePosition = null;
    map.render();
});

또한 postcompose 이벤트의 지정도 필요합니다. 이 이벤트는 레이어가 그려지고 난 뒤에 호출되는 이벤트인데요. 앞서 precompose 이벤트에서 수행한 잘라내기(Clipping)를 무효화해줍니다. 코드는 아래와 같습니다.

imagery.on('postcompose', function (event) {
    var ctx = event.context;
    ctx.restore();
});

이제 실행해보면, 레이어 스파이 기능이 작동되는 것을 확인할 수 있습니다. 아래는 지금까지에 대한 전체 코드입니다.

[OpenLayers3] WMS의 GetFeatureInfo 사용하기

WMS는 웹 환경에서 지도를 서비스하는 것으로, 클라이언트가 요청한 영역에 대한 지도를 서버측에서 이미지로 생성해 제공해 주는 서비스입니다. 이 WMS의 기능 중 GetFeatureInfo라는 것이 있는데요. 특정 좌표 지점에 위치한 Feature의 정보를 클라이언트에게 제공해 주는 서비스입니다. 이 기능은 사용자가 지도 상에 클릭한 지점에 대한 정보를 제공하기 위한 용도로 가장 많이 사용됩니다. 아래의 지도에서 임이의 나라를 클릭해 보면 해당 나라의 정보가 지도 하단에 표시되는 것을 볼 수 있습니다.

 

위의 페이지를 ol3에서 어떻게 구현하는지 살펴 보겠습니다. 먼저 필요한 외부 css와 js 파일을 아래처럼 페이지에 포함합니다.




그리고 UI에 대한 Tag를 아래처럼 추가합니다.

id가 map인 div는 지도가 표시될 영역이고, id가 info인 div에는 사용자가 클릭한 지점에 위치한 나라의 정보를 표시합니다.

이제 스크립트 코드를 살펴보겠습니다. 먼저 jQuery의 ready 이벤트를 아래처럼 준비해 둡니다.


위의 … 부분에 앞으로의 모든 스크립트가 추가됩니다. 먼저 추가할 코드는 WMS에 대한 소스와 레이어 객체의 생성입니다. WMS 레이어는 데이터를 받을 소스가 필요합니다. 아래의 코드가 바로 그것입니다.

var wmsSource = new ol.source.TileWMS({
    url: 'https://ahocevar.com/geoserver/wms',
    params: { 'LAYERS': 'ne:ne' },
    serverType: 'geoserver',
    crossOrigin: 'anonymous'
});

var wmsLayer = new ol.layer.Tile({
    source: wmsSource
});

그리고 다음은 초기 지도가 표시될때 지도 화면 중심과 줌 레벨에 대한 View 객체입니다. 이 객체를 통해 WMS의 GetFeatureInfo 기능을 사용할때 필요한 현재 지도의 해상도값을 얻어올 수 있습니다.

var view = new ol.View({
    center: [0, 0],
    zoom: 3
});

WMS를 활용한 레이어 객체를 통해 다음의 코드처럼 지도 객체를 생성하는데요. 생성시 옵션값인 view에 위의 코드에서 생성한 view 객체를 할당해 주고 있습니다. 그리고 target 옵션에는 지도가 표시될 div의 id 값이 지정됩니다.

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

이제 다음은 지도를 클릭하면 클릭한 지점에 대한 정보를 얻기 위한 WMS의 GetFeatureInfo 기능을 호출하는 기능에 대한 코드입니다. 코드는 아래와 같습니다.

map.on('singleclick', function (evt) {
    document.getElementById('info').innerHTML = '';
    
    var viewResolution = view.getResolution();
    var url = wmsSource.getGetFeatureInfoUrl(
        evt.coordinate, viewResolution, 'EPSG:3857',
        { 'INFO_FORMAT': 'text/html' }
    );
    
    if (url) {
        document.getElementById('info').innerHTML =
            '';
    }
});

지도 상의 클릭 이벤트로 singleclick 이라는 특화된 이벤트를 지도 객체에 등록하고 있습니다. 이 클릭 이벤트 함수에는 evt라는 객체가 넘어오는데요. evt의 coordinate 속성에 사용자가 클릭한 지점의 지도 좌표계 값이 담겨 있습니다. 가장 중요한 부분이 5번 코드인데요. 앞서 WMS를 위한 소스 객체인 wmsSource의 getGetFeatureInfoUrl이라는 함수를 호출하면 WMS의 GetFeatureInfo 기능을 위한 호출 가능한 URL이 문자열로 반환됩니다. 이 getGetFeatureInfoUrl 함수를 호출하기 위해서 필요한 인자는 사용자가 클릭한 지도 상의 좌표와 현재 지도의 줌 레벨에 대한 해상도값과 클릭한 좌표에 대한 좌표체계, 끝으로 GetFeatureInfo 서비스 호출 시 받을 데이터의 포맷인데요. getGetFeatureInfoUrl 함수의 호출 시 지정된 인자값들이 바로 이 인자들입니다. 만약 getGetFeatureInfoUrl 함수의 호출이 성공하면 url 문자열이 반환되고 이 url을 id가 info인 div에 innerHTML 값으로 iframe의 src 속성에 지정합니다. 코드는 단순한데 설명이 복잡합니다. 이 설명은 10번~13번 코드에 대한 설명이니 코드를 보시면 이해가 될 것입니다.

이상으로 필요한 코드를 전부 살펴보았습니다. 지금까지 작성한 전체 코드는 아래 url을 통해 다운로드 받을 수 있습니다.