[OpenLayers] 공간 데이터에 대한 Cluster 시각화

아래와 같은 위치를 표시하는 포인트 레이어가 있다.

이때 이 무수히 많은 포인트를 군집화해서(Clustering) 표현하면 아래와 같은 결과를 볼 수 있다.

일정한 거리를 기준으로 군집화, 즉 그룹으로 묶어서 묶여진 개수와 함께 표시하고 있다. 표시 속도 역시 빠르다. 이러한 공간 데이터에 대한 Cluster 시각화를 OpenLayers에서 구현하는 API에 대해 정리하는 글이다.

먼저 DOM 요소를 생성한데, 지도에 대한 div와 군집화를 위한 거리값을 입력 받기 위한 DOM이며 아래와 같다.



    
        
        OpenLayers
        
    

    

JS 코드는 index.js에서 작성되데, 먼저 OpenLayers 즉, ol에서 필요로 하는 모듈을 아래처럼 추가한다.

import Feature from 'ol/Feature.js';
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import Point from 'ol/geom/Point.js';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer.js';
import {Cluster, OSM, Vector as VectorSource} from 'ol/source.js';
import {Circle as CircleStyle, Fill, Stroke, Style, Text} from 'ol/style.js';

다음은 필요한 변수를 정의한다.

var distance = document.getElementById('distance');

var count = 20000;
var features = new Array(count);
var e = 4500000;

distance 변수는 군집화를 위한 거리값을 입력받는 DOM 요소이고, count는 군집화 대상이 되는 포인트 개수이다. 그리고 features는 군집화 대상이 되는 포인트에 대한 Feature이다. Feature는 좌표값과 속성값을 갖으며 VectorSource의 구성 요소이다. e는 군집화 대상이 되는 포인트의 위치를 무작위로 결정하기 위한 범위값이다. 아래는 군집화 대상이 되는 포인트 Feature를 생성하는 코드이다.

for (var i = 0; i < count; ++i) {
    var coordinates = [2 * e * Math.random() - e, 2 * e * Math.random() - e];
    features[i] = new Feature(new Point(coordinates));
}

이렇게 생성된 Feature들에 대한 데이터 소스와 Cluster 데이터 소스를 아래처럼 생성한다.

var source = new VectorSource({
    features: features
});

var clusterSource = new Cluster({
    distance: parseInt(distance.value, 10),
    source: source
});

지도의 구성 요소로써 화면에 표시되기 위해 레이어가 필요하며, 데이터소스는 레이어를 생성하는데 이용된다. 앞서 생성한 데이터소스를 아래의 코드처럼 활용하여 레이어를 생성한다.

var styleCache = {};
var clusters = new VectorLayer({
    source: clusterSource,
    style: function(feature) {
        var size = feature.get('features').length;
        var style = styleCache[size];
    
        if (!style) {
            style = new Style({
                image: new CircleStyle({
                    radius: 10,
                    stroke: new Stroke({
                        color: '#fff'
                    }),
                    fill: new Fill({
                        color: '#3399CC'
                    })
                }),
                text: new Text({
                    text: size.toString(),
                    fill: new Fill({
                        color: '#fff'
                    })
                })
            });
    
            styleCache[size] = style;
        }

        return style;
    }
});

아래의 코드처럼 지도를 생성하고 앞서 생성된 레이어를 지도의 구성 요소로 정의한다. 배경지도로써 OpenStreetMap도 또 다른 레이어로 활용했다.

var raster = new TileLayer({
    source: new OSM()
});

var map = new Map({
    layers: [raster, clusters],
    target: 'map',
    view: new View({
        center: [0, 0],
        zoom: 2
    })
});

마지막으로 클러스터를 위한 거리값 지정을 위한 DOM에 아래와 같은 이벤트를 추가하여 언제든 사용자가 거리값을 변경하여 클러스터링에 반영할 수 있도록 한다.

distance.addEventListener('input', function() {
    clusterSource.setDistance(parseInt(distance.value, 10));
});

이제 실행하면 그 결과를 볼 수 있다.

[Python] pip 명령어

PIP는 Pythond의 Package 관리자이다. 개발언어인 파이선에서 패키지는 라이브러리 개념과 동일하다. PIP를 통한 패키지를 관리하기 위한 명령을 정리한다.

# pip 업데이트 (Linux)

pip install pip --upgrade

# pip 업데이트 (Windows)

python -m pip install --upgrade pip

# Python 설치(opencv-contrib-python 패키지)

pip install opencv-contrib-python

# Update 해야할 패키지 목록

pip list -o

# numpy 패키지 설치

pip install numpy

# numpy 패키지 업데이트

pip install numpy --upgrade

# numpy 패키지 제거

pip uninstall numpy

# 특정 버전의 (또는 contrib 버전) opencv 패키지 설치(참조 url: https://pypi.org/project/opencv-contrib-python/3.4.5.20/)

pip install opencv-contrib-python==3.4.5.20

[OpenLayers] 클릭한 지점에 Overlay 생성

map의 sinlgeclick 이벤트를 등록하고, 실행되는 코드를 다음처럼 지정합니다.

import Overlay from 'ol/Overlay.js';

map.on('singleclick', function(evt) {
    let container = document.createElement('div');
    container.classList.add('ol-popup-custom');

    let content = document.createElement('div');
    content.classList.add('popup-content');

    container.appendChild(content);
    document.body.appendChild(container);

    var coordinate = evt.coordinate; // 클릭한 지도 좌표
    content.innerHTML = '' + '한글(KOR)입니다.' + '';

    var overlay = new Overlay({
        element: container,
        //autoPan: true,
        //autoPanAnimation: {
        //  duration: 250
        //}
      });

    map.addOverlay(overlay);

    overlay.setPosition(coordinate);
});

참고로 위의 코드에 언급된 CSS에 대한 코드는 다음과 같습니다.

.ol-popup-custom {
    padding: 0;
    margin: 0;
    pointer-events: none;
    position: absolute;
    background-color: white;
    filter: drop-shadow(3px 3px 7px rgba(0,0,0,0.6));
    border: 1px solid black;
    width: 90px;
    left: -45px; /* 위치를 조정, -width의 절반값 */
    height: 24px;
    bottom: -12px; /* 위치를 조정, -height의 절반값 */
    box-sizing: border-box;
    overflow: hidden;
}

.popup-content {
   position: relative;
   animation-duration: 1s;
   animation-name: slidein;
   animation-direction: alternate;
   animation-iteration-count: infinite;
   box-sizing: border-box;
   line-height: 100%;
}

.popup-content > span {
    display: inline-block;
    line-height: 100%;
    width: 100%;
    font-size:10px;
    font-weight: bold;
    box-sizing: border-box;
}

@keyframes slidein {
    from {
        left: 90px;
    }

    to {
        left: 0px;
    }
}

실행하고 지도를 클릭해 보면, 아래처럼 클릭한 지점에 Overlay 정보가 표시되는 것을 볼 수 있습니다.

[OpenLayers] 클릭된 Feature의 속성 얻기

두가지 방법이 있는데, 첫째는 지도 객체의 forEachFeatureAtPixel을 이용하는 방식이며 아래의 예와 같다.

map.on(
    "click", 
    function(e) {
        map.forEachFeatureAtPixel(
            e.pixel, 
            function (feature, layer) {
                let values = feature.getProperties();
                // values에 속성값이 담겨 있음(예: values["fieldName"])
            }, 
            {
                hitTolerance: 2,
                layerFilter: function(layer) {
                    return layer === buildingLayer;
                }
            }
        );
    }
);

두번째 방법은 Select 클래스를 이용하는 방법이며 아래의 예와 같다.

import Select from 'ol/interaction/Select.js';

var select = new Select({
    layers: [buildingLayer],

    /*
    // 위의 layers 옵션 지정과 동일한 방식인데, 속도면에서는 layers 옵션을 통한 방식이 더 빠를 것으로 예상됨
    filter: function(feature, layer) {
        return buildingLayer === layer;
    }
    */
});

select.getFeatures().on(
    "add", 
    function (e) { 
        var feature = e.element;

        setTimeout(function() {
            select.getFeatures().clear();
        }, 2000); // 2초 뒤에 선택된 상태가 해제됨
    }
);
       
map.addInteraction(select);

참고로 선택 대상이 되는 레이어의 타입은 VectorTileLayer 이다.