ol은 그 자체의 기능을 확장시키기 위한 확장 기능이 존재합니다. 그 중 하나가 바로 ol-ext이고, 이 곳에서 제공하는 AnimatedCluster 기능에 대해 살펴 보겠습니다.
먼저 ol-ext 기능을 ES6의 모듈방식으로 개발하기 위해 다음과 같은 ol-ext 모듈 설치가 필요합니다.
npm install ol-ext
이제 ol에도 이미 Cluster 기능이 존재하는데, 이 Cluster에 시각적인 효과를 추가한 것이 AnimatedCluster 입니다. 이에 대한 예제를 만들어 만들어 갈건데요. 먼저 완성된 예제의 실행 결과를 동영상으로 살펴보면 아래와 같습니다.
위의 결과는 지도 상에 10000개의 포인트 피쳐를 추가하여 인접한 포인트 간의 클러스터링을 적용하여 시각화하고 있습니다. 이제 예제 코드를 하나씩 살펴보겠습니다.
먼저 index.html 파일입니다.
OpenLayers
지도를 위한 div 하나와 index.js에 대한 스크립트 파일입니다. index.js 파일의 코드를 하나씩 살펴보면.. 먼저 필요한 모듈을 추가합니다.
import 'ol/ol.css'; 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 VectorSource from 'ol/source/Vector.js'; import {Fill, Stroke, Style, Circle, Text} from 'ol/style.js'; import {Tile} from 'ol/layer.js'; import {XYZ, Cluster} from 'ol/source.js'; import AnimatedCluster from 'ol-ext/layer/AnimatedCluster.js' import SelectCluster from 'ol-ext/interaction/SelectCluster.js'
먼저 10000개의 포인트 피쳐를 가지는 VectorSource 객체를 생성합니다. 이 벡터소스가 클러스터링 대상이 되는 데이터의 제공자입니다.
var featureCount = 10000; var features = new Array(featureCount); var feature, geometry; for (var i = 0; i < featureCount; ++i) { geometry = new Point([14078579 + Math.random()*100000, 4487570 + Math.random()*50000]); feature = new Feature(geometry); feature.set('id', i); features[i] = feature; } var vectorSource = new VectorSource({ features: features });
위의 벡터소스를 통해 클러스터링 알고리즘이 적용되어 또 다른 데이터소스가 만들어지게 됩니다. 아래의 코드는 클러스터링 알고리즘이 적용되어 만들어지는 데이터소스 객체와 이 데이터소스를 기반으로 하는 레이어로써 AnimatedCluster 객체를 생성합니다.
var clusterSource = new Cluster({ distance: 200, source: vectorSource }); var clusterLayer = new AnimatedCluster({ name: 'Cluster', source: clusterSource, animationDuration: 400, style: getStyle });
clusterSource 객체를 생성하기 위한 옵션으로 distance는 화면의 px 단위 좌표로써 해당 px 값 범위 안의 포인트 요소를 하나의 그룹으로 묶는 기준값입니다. clusterLayer 객체 생성을 위한 옵션으로 animationDuration은 지도를 확대, 축소시 포인트 요소가 클러스터링 되는 것을 애니메이션으로 시각화해주는 시간을 ms 단위로 지정하며 style은 클래스터링 된 포인트 피쳐를 어떤 모양으로 지도 상에 표시할 것인지를 결정하는 함수입니다. getStyle이라는 함수가 지정되어 있는데, 해당 함수는 다음과 같습니다.
var styleCache = {}; function getStyle (feature, resolution) { var size = feature.get('features').length; var style = styleCache[size]; if (!style) { var color = size>25 ? '192,0,0' : size>8 ? '255,128,0' : '0,128,0'; var radius = Math.max(8, Math.min(size*0.75, 20)); var dash = 2*Math.PI*radius/6; var dash = [ 0, dash, dash, dash, dash, dash, dash ]; style = styleCache[size] = new Style({ image: new Circle({ radius: radius, stroke: new Stroke({ color: 'rgba('+color+',0.5)', width: 15, lineDash: dash, lineCap: 'butt' }), fill: new Fill({ color:'rgba('+color+',1)' }) }), text: new Text({ text: size.toString(), fill: new Fill({ color: '#fff' }), font: '10px Arial', }) }); } return [style]; }
이제 지도 객체를 생성합니다. 지도 객체는 다수의 레이어로 구성되어 있으며 앞서 생성한 클러스터 레이어와 함께 배경지도로써 VWorld의 TMS 배경지도 레이어로 구성합니다.
var base = new Tile({ source: new XYZ({ url: 'http://xdworld.vworld.kr:8080/2d/Base/service/{z}/{x}/{y}.png', }) }); var map = new Map({ layers: [base, clusterLayer], target: document.getElementById('map'), view: new View({ center: [14128579.82, 4512570.74], zoom: 15 }) });
여기까지만 실행해도 시각적인 클러스터링은 모두 완성된 것입니다. 여기에 더해 클러스터링 된 포인트 피처를 마우스나 터치로 선택할 경우 해당 피쳐의 ID값이나 선택된 그룹(부모) 피쳐가 몇개의 자식 피쳐로 구성되는지를 파악하는 코드를 추가해 보겠습니다.
부모 피쳐를 마우스나 터치로 선택하면 구성되는 자식 포인트 피쳐를 원형으로 펼쳐 표시하는데, 이때 사용되는 스타일 객체를 다음처럼 생성합니다.
// 자식 포인트의 스타일 var img = new Circle({ radius: 5, stroke: new Stroke({ color: 'rgba(0,255,255,1)', width: 1 }), fill: new Fill({ color: 'rgba(0,255,255,0.3)' }) }); // 자식 포인트와 부모 피쳐 사이에 그릴 선에 대한 스타일 var linkStyle = new Style({ image: img, stroke: new Stroke({ color: '#fff', width: 1 }) });
확장 기능으로써 AnimatedCluster에 대한 선택 기능 역시 확장 기능으로써 SelectCluster라는 인터랙션(Interaction)으로 수행됩니다. 아래의 코드가 이 인터랙션 객체를 생성합니다.
var selectCluster = new SelectCluster({ // 부모를 클릭하여 자식이 표시될때 부모와 자식간의 거리(px 단위) pointRadius:7, animate: true, // 부모와 자식 사이에 그려질 선에 대한 스타일 featureStyle: function() { return [ linkStyle ]; }, // 부모가 선택된 상태에서 다시 부모와 자식이 선택될때 선택된 요소의 스타일 style: function(f, res) { var cluster = f.get('features'); if (cluster.length>1) { // 부모 스타일 return getStyle(f,res); } else { // 자식 스타일 return [ new Style({ image: new Circle({ stroke: new Stroke({ color: 'rgba(0,0,192,0.5)', width:2 }), fill: new Fill({ color: 'rgba(0,0,192,0.3)' }), radius: 5 }) }) ]; } } });
부모나 자식이 선택될 경우에 이벤트를 받아 처리하여 원하는 기능을 원할하게 추가해야 합니다. 아래의 코드처럼요.
selectCluster.getFeatures().on(['add'], function (e) { var c = e.element.get('features'); if (c.length==1) { // 자식을 선택하때, 자식의 id 속성을 표시 var feature = c[0]; console.log('Selected Feature Id = ' + feature.get('id')); } else { // 부모를 선택할때, 부모가 가진 자식의 개수를 표시 console.log('Count = ' + c.length); } }); selectCluster.getFeatures().on(['remove'], function (e) { //. });
마지막으로 생성한 인터렉션을 받을 지도에 추가합니다.
map.addInteraction(selectCluster);