Source: uGisLayer/uGisClusterLayer.js

( function() {
	"use strict";

	/**
	 * Cluster 레이어 객체.
	 * 
	 * Cluster 데이터를 표현할 수 있는 레이어 객체.
	 * 
	 * @todo ★View 좌표계 변경에 따른 피처 좌표계 변환★ 기능 개발
	 * 
	 * @example
	 * 
	 * <pre>
	 * var ugVectorLayer = new ugmp.layer.uGisClusterLayer( {
	 * distance : 50,
	 * features : [ new ol.Feature( {
	 * 	geometry : new ol.geom.Point({...})
	 * } ) ],
	 * useAnimation : true,
	 * style : new ol.style.Style({...})
	 * } );
	 * </pre>
	 * 
	 * @constructor
	 * 
	 * @param opt_options {Object}
	 * @param opt_options.distance {Number} 클러스터 사이의 거리. Default is `50`.
	 * @param opt_options.useAnimation {Boolean} 애니메이션 효과 사용 여부. Default is `true`.
	 * @param opt_options.features {Array<ol.Feature>|ol.Collection} 대상 피처 리스트.
	 * @param opt_options.style {ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction} 스타일.
	 * 
	 * @Extends {ugmp.layer.uGisLayerDefault}
	 * 
	 * @class
	 */
	ugmp.layer.uGisClusterLayer = ( function(opt_options) {
		var _self = this;
		var _super = null;

		this.style = null;
		this.distance = null;
		this.features = null;

		this.clusters = null;
		this.animation = null;
		this.oldcluster = null;
		this.useAnimation = null;


		/**
		 * Initialize
		 */
		( function() {

			var options = opt_options || {};

			options.layerType = "Cluster";
			options.useGetFeature = false;

			_super = ugmp.layer.uGisLayerDefault.call( _self, options );

			_self.clusters = [];
			_self.oldcluster = new ol.source.Vector();
			_self.animation = {
				start : false
			};

			_self.style = ( options.style !== undefined ) ? options.style : _self._defaultStyle;
			_self.features = ( options.features !== undefined ) ? options.features : [];
			_self.distance = ( typeof ( options.distance ) === "number" ) ? options.distance : 50;
			_self.useAnimation = ( typeof ( options.useAnimation ) === "boolean" ) ? options.useAnimation : true;

			_self._init();

		} )();
		// END Initialize


		return ugmp.util.uGisUtil.objectMerge( _super, {
			_this : _self,
			setDistance : _self.setDistance,
			getFeatures : _self.getFeatures,
			setUseAnimation : _self.setUseAnimation
		} );

	} );


	ugmp.layer.uGisClusterLayer.prototype = Object.create( ugmp.layer.uGisLayerDefault.prototype );
	ugmp.layer.uGisClusterLayer.prototype.constructor = ugmp.layer.uGisClusterLayer;


	/**
	 * init
	 * 
	 * @private
	 */
	ugmp.layer.uGisClusterLayer.prototype._init = function(state_) {
		var _self = this._this || this;

		_self.olLayer = new ol.layer.Vector( {
			source : new ol.source.Cluster( {
				distance : _self.distance,
				source : new ol.source.Vector( {
					features : _self.features
				} )
			} ),
			style : _getStyle
		} );


		_self.setUseAnimation( _self.useAnimation );
		_self.olLayer.set( 'animationMethod', ol.easing.easeOut );

		// Save cluster before change
		_self.olLayer.getSource().on( 'change', _self._saveCluster, _self );
		// Animate the cluster
		_self.olLayer.on( 'precompose', _self._animate, _self );
		_self.olLayer.on( 'postcompose', _self._postanimate, _self );


		var styleCache = {};
		function _getStyle(feature, resolution) {
			var size = feature.get( 'features' ).length;
			var style = styleCache[ size ];
			if ( !style ) {
				if ( typeof _self.style === "function" ) {
					style = _self.style.call( this, feature, resolution );
				} else {
					style = _self.style;
				}
				styleCache[ size ] = style;
			}
			return [ style ];
		}
	};


	/**
	 * 기본 스타일
	 * 
	 * @private
	 */
	ugmp.layer.uGisClusterLayer.prototype._defaultStyle = function(feature, resolution) {
		var size = feature.get( 'features' ).length;
		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 ];
		var style = new ol.style.Style( {
			image : new ol.style.Circle( {
				radius : radius,
				stroke : new ol.style.Stroke( {
					color : "rgba(" + color + ",0.5)",
					width : 15,
					lineDash : dash,
					lineCap : "butt"
				} ),
				fill : new ol.style.Fill( {
					color : "rgba(" + color + ",1)"
				} )
			} ),
			text : new ol.style.Text( {
				text : size.toString(),
				fill : new ol.style.Fill( {
					color : '#fff'
				} )
			} )
		} );

		return style;
	};


	/**
	 * 클러스터 애니메이션 효과 사용 여부 설정.
	 * 
	 * @param state {Boolean} 애니메이션 효과 사용 여부.
	 */
	ugmp.layer.uGisClusterLayer.prototype.setUseAnimation = function(state_) {
		var _self = this._this || this;
		_self.olLayer.set( 'animationDuration', state_ ? 700 : 0 );
	};


	/**
	 * 클러스터 사이의 거리 설정.
	 * 
	 * @param distance {Number} 클러스터 사이의 거리.
	 */
	ugmp.layer.uGisClusterLayer.prototype.setDistance = function(distance_) {
		var _self = this._this || this;

		var source = _self.olLayer.getSource();
		ol.source.Cluster.prototype.setDistance.call( source, distance_ );
	};


	/**
	 * 레이어의 Feature 리스트를 가져온다.
	 * 
	 * @return features {Array.<ol.Feature>} 피처 리스트.
	 */
	ugmp.layer.uGisClusterLayer.prototype.getFeatures = function() {
		var _self = this._this || this;
		return _self.olLayer.getSource().getSource().getFeatures();
	};


	/**
	 * _saveCluster
	 * 
	 * @private
	 */
	ugmp.layer.uGisClusterLayer.prototype._saveCluster = function() {
		var _self = this._this || this;

		_self.oldcluster.clear();
		if ( !_self.olLayer.get( 'animationDuration' ) ) return;

		var features = _self.olLayer.getSource().getFeatures();
		if ( features.length && features[ 0 ].get( 'features' ) ) {
			_self.oldcluster.addFeatures( _self.clusters );
			_self.clusters = features.slice( 0 );
			_self.sourceChanged = true;
		}
	};


	/**
	 * Get the cluster that contains a feature
	 * 
	 * @private
	 */
	ugmp.layer.uGisClusterLayer.prototype._getClusterForFeature = function(f, cluster) {
		var _self = this._this || this;

		for ( var j = 0 , c; c = cluster[ j ]; j++ ) {
			var features = cluster[ j ].get( 'features' );

			if ( features && features.length ) {
				for ( var k = 0 , f2; f2 = features[ k ]; k++ ) {
					if ( f === f2 ) {
						return cluster[ j ];
					}
				}
			}
		}
		return false;
	};


	/**
	 * _stopAnimation
	 * 
	 * @private
	 */
	ugmp.layer.uGisClusterLayer.prototype._stopAnimation = function() {
		var _self = this._this || this;
		_self.animation.start = false;
		_self.animation.cA = [];
		_self.animation.cB = [];
	};


	/**
	 * animate the cluster
	 * 
	 * @private
	 */
	ugmp.layer.uGisClusterLayer.prototype._animate = function(e) {
		var _self = this._this || this;

		var duration = _self.olLayer.get( 'animationDuration' );
		if ( !duration ) return;

		var resolution = e.frameState.viewState.resolution;
		var a = _self.animation;
		var time = e.frameState.time;

		// Start a new animation, if change resolution and source has changed
		if ( a.resolution != resolution && _self.sourceChanged ) {
			var extent = e.frameState.extent;

			if ( a.resolution < resolution ) {
				extent = ol.extent.buffer( extent, 100 * resolution );
				a.cA = _self.oldcluster.getFeaturesInExtent( extent );
				a.cB = _self.olLayer.getSource().getFeaturesInExtent( extent );
				a.revers = false;
			} else {
				extent = ol.extent.buffer( extent, 100 * resolution );
				a.cA = _self.olLayer.getSource().getFeaturesInExtent( extent );
				a.cB = _self.oldcluster.getFeaturesInExtent( extent );
				a.revers = true;
			}

			a.clusters = [];

			for ( var i = 0 , c0; c0 = a.cA[ i ]; i++ ) {
				var f = c0.get( 'features' );
				if ( f && f.length ) {
					var c = _self._getClusterForFeature( f[ 0 ], a.cB );
					if ( c ) a.clusters.push( {
						f : c0,
						pt : c.getGeometry().getCoordinates()
					} );
				}
			}

			// Save state
			a.resolution = resolution;
			_self.sourceChanged = false;

			// No cluster or too much to animate
			if ( !a.clusters.length || a.clusters.length > 1000 ) {
				_self._stopAnimation();
				return;
			}
			// Start animation from now
			time = a.start = ( new Date() ).getTime();
		}

		// Run animation
		if ( a.start ) {
			var vectorContext = e.vectorContext;
			var d = ( time - a.start ) / duration;

			// Animation ends
			if ( d > 1.0 ) {
				_self._stopAnimation();
				d = 1;
			}
			d = _self.olLayer.get( 'animationMethod' )( d );

			// Animate
			var style = _self.olLayer.getStyle();
			var stylefn = ( typeof ( style ) == 'function' ) ? style : style.length ? function() {
				return style;
			} : function() {
				return [ style ];
			};

			// Layer opacity
			e.context.save();
			e.context.globalAlpha = _self.olLayer.getOpacity();

			// Retina device
			var ratio = e.frameState.pixelRatio;

			for ( var i = 0 , c; c = a.clusters[ i ]; i++ ) {
				var pt = c.f.getGeometry().getCoordinates();

				if ( a.revers ) {
					pt[ 0 ] = c.pt[ 0 ] + d * ( pt[ 0 ] - c.pt[ 0 ] );
					pt[ 1 ] = c.pt[ 1 ] + d * ( pt[ 1 ] - c.pt[ 1 ] );
				} else {
					pt[ 0 ] = pt[ 0 ] + d * ( c.pt[ 0 ] - pt[ 0 ] );
					pt[ 1 ] = pt[ 1 ] + d * ( c.pt[ 1 ] - pt[ 1 ] );
				}

				// Draw feature
				var st = stylefn( c.f, resolution );
				/* Preserve pixel ration on retina */
				var s;
				var geo = new ol.geom.Point( pt );
				for ( var k = 0; s = st[ k ]; k++ ) {
					var sc;
					// OL < v4.3 : setImageStyle doesn't check retina
					var imgs = ol.Map.prototype.getFeaturesAtPixel ? false : s.getImage();
					if ( imgs ) {
						sc = imgs.getScale();
						imgs.setScale( sc * ratio );
					}
					// OL3 > v3.14
					if ( vectorContext.setStyle ) {
						vectorContext.setStyle( s );
						vectorContext.drawGeometry( geo );
					}
					// older version
					else {
						vectorContext.setImageStyle( imgs );
						vectorContext.setTextStyle( s.getText() );
						vectorContext.drawPointGeometry( geo );
					}
					if ( imgs ) imgs.setScale( sc );
				}
			}

			e.context.restore();
			// tell OL3 to continue postcompose animation
			e.frameState.animate = true;

			// Prevent layer drawing (clip with null rect)
			e.context.save();
			e.context.beginPath();
			e.context.rect( 0, 0, 0, 0 );
			e.context.clip();
			_self.clip_ = true;
		}

		return;
	};


	/**
	 * remove clipping after the layer is drawn
	 * 
	 * @private
	 */
	ugmp.layer.uGisClusterLayer.prototype._postanimate = function(e) {
		var _self = this._this || this;
		if ( _self.clip_ ) {
			e.context.restore();
			_self.clip_ = false;
		}
	};

} )();