358 lines
13 KiB
JavaScript
358 lines
13 KiB
JavaScript
/*
|
|
Copyright (c) 2015 Jean-Marc VIGLINO,
|
|
released under the CeCILL-B license (http://www.cecill.info/).
|
|
|
|
ol/interaction/SelectCluster is an interaction for selecting vector features in a cluster.
|
|
*/
|
|
|
|
import ol_Map from 'ol/Map.js'
|
|
import ol_Collection from 'ol/Collection.js'
|
|
import ol_layer_Vector from 'ol/layer/Vector.js'
|
|
import ol_source_Vector from 'ol/source/Vector.js'
|
|
import ol_interaction_Select from 'ol/interaction/Select.js'
|
|
import ol_Feature from 'ol/Feature.js'
|
|
import ol_geom_LineString from 'ol/geom/LineString.js'
|
|
import {unByKey as ol_Observable_unByKey} from 'ol/Observable.js'
|
|
import {easeOut as ol_easing_easeOut} from 'ol/easing.js'
|
|
import ol_geom_Point from 'ol/geom/Point.js'
|
|
import ol_style_Style from 'ol/style/Style.js'
|
|
import ol_style_Circle from 'ol/style/Circle.js'
|
|
import ol_render_getVectorContext from '../util/getVectorContext.js';
|
|
import { createEmpty as ol_extent_createEmpty } from 'ol/extent.js'
|
|
import { extend as ol_extent_extend } from 'ol/extent.js'
|
|
import { singleClick as ol_events_condition_singleClick } from 'ol/events/condition.js';
|
|
import {getUserProjection as ol_proj_getUserProjection} from 'ol/proj.js'
|
|
|
|
/**
|
|
* @classdesc
|
|
* Interaction for selecting vector features in a cluster.
|
|
* It can be used as an ol.interaction.Select.
|
|
* When clicking on a cluster, it springs apart to reveal the features in the cluster.
|
|
* Revealed features are selectable and you can pick the one you meant.
|
|
* Revealed features are themselves a cluster with an attribute features that contain the original feature.
|
|
*
|
|
* @constructor
|
|
* @extends {ol.interaction.Select}
|
|
* @param {olx.interaction.SelectOptions=} options SelectOptions.
|
|
* @param {ol.style} options.featureStyle used to style the revealed features as options.style is used by the Select interaction.
|
|
* @param {boolean} options.selectCluster false if you don't want to get cluster selected
|
|
* @param {Number} options.pointRadius to calculate distance between the features
|
|
* @param {bool} options.spiral means you want the feature to be placed on a spiral (or a circle)
|
|
* @param {Number} options.circleMaxObjects number of object that can be place on a circle
|
|
* @param {Number} options.maxObjects number of object that can be drawn, other are hidden
|
|
* @param {bool} options.animate if the cluster will animate when features spread out, default is false
|
|
* @param {Number} options.animationDuration animation duration in ms, default is 500ms
|
|
* @param {boolean} options.autoClose if selecting a cluster should close previously selected clusters. False to get toggle feature. Default is true
|
|
* @fires ol.interaction.SelectEvent
|
|
* @api stable
|
|
*/
|
|
var ol_interaction_SelectCluster = class olinteractionSelectCluster extends ol_interaction_Select {
|
|
constructor(options) {
|
|
options = options || {}
|
|
|
|
// Create a new overlay layer for
|
|
var overlay = new ol_layer_Vector({
|
|
source: new ol_source_Vector({
|
|
features: new ol_Collection(),
|
|
wrapX: options.wrapX,
|
|
useSpatialIndex: true
|
|
}),
|
|
name: 'Cluster overlay',
|
|
updateWhileAnimating: true,
|
|
updateWhileInteracting: true,
|
|
displayInLayerSwitcher: false,
|
|
style: options.featureStyle
|
|
})
|
|
|
|
// Add the overlay to selection
|
|
if (options.layers) {
|
|
if (typeof (options.layers) == "function") {
|
|
var fnLayers = options.layers
|
|
options.layers = function (layer) {
|
|
return (layer === overlay || fnLayers(layer))
|
|
}
|
|
} else if (options.layers.push) {
|
|
options.layers.push(overlay)
|
|
}
|
|
}
|
|
|
|
// Don't select links
|
|
if (options.filter) {
|
|
var fnFilter = options.filter
|
|
options.filter = function (f, l) {
|
|
//if (l===overlay && f.get("selectclusterlink")) return false;
|
|
if (!l && f.get("selectclusterlink"))
|
|
return false
|
|
else
|
|
return fnFilter(f, l)
|
|
}
|
|
} else
|
|
options.filter = function (f, l) {
|
|
//if (l===overlay && f.get("selectclusterlink")) return false;
|
|
if (!l && f.get("selectclusterlink"))
|
|
return false
|
|
else
|
|
return true
|
|
}
|
|
|
|
if ((options.autoClose === false) && !options.toggleCondition) {
|
|
options.toggleCondition = ol_events_condition_singleClick
|
|
}
|
|
|
|
super(options)
|
|
|
|
this.overlayLayer_ = overlay;
|
|
this.filter_ = options.filter
|
|
this.pointRadius = options.pointRadius || 12
|
|
this.circleMaxObjects = options.circleMaxObjects || 10
|
|
this.maxObjects = options.maxObjects || 60
|
|
this.spiral = (options.spiral !== false)
|
|
this.animate = options.animate
|
|
this.animationDuration = options.animationDuration || 500
|
|
this.selectCluster_ = (options.selectCluster !== false)
|
|
this._autoClose = (options.autoClose !== false)
|
|
|
|
this.on("select", this.selectCluster.bind(this))
|
|
}
|
|
/**
|
|
* Remove the interaction from its current map, if any, and attach it to a new
|
|
* map, if any. Pass `null` to just remove the interaction from the current map.
|
|
* @param {ol.Map} map Map.
|
|
* @api stable
|
|
*/
|
|
setMap(map) {
|
|
if (this.getMap()) {
|
|
this.getMap().removeLayer(this.overlayLayer_)
|
|
}
|
|
if (this._listener)
|
|
ol_Observable_unByKey(this._listener)
|
|
this._listener = null
|
|
|
|
super.setMap(map)
|
|
this.overlayLayer_.setMap(map)
|
|
// map.addLayer(this.overlayLayer_);
|
|
if (map && map.getView()) {
|
|
this._listener = map.getView().on('change:resolution', this.clear.bind(this))
|
|
}
|
|
}
|
|
/**
|
|
* Clear the selection, close the cluster and remove revealed features
|
|
* @api stable
|
|
*/
|
|
clear() {
|
|
this.getFeatures().clear()
|
|
this.overlayLayer_.getSource().clear()
|
|
}
|
|
/**
|
|
* Get the layer for the revealed features
|
|
* @api stable
|
|
*/
|
|
getLayer() {
|
|
return this.overlayLayer_
|
|
}
|
|
/**
|
|
* Select a cluster
|
|
* @param {ol.SelectEvent | ol.Feature} a cluster feature ie. a feature with a 'features' attribute.
|
|
* @api stable
|
|
*/
|
|
selectCluster(e) {
|
|
// It's a feature => convert to SelectEvent
|
|
if (e instanceof ol_Feature) {
|
|
e = { selected: [e] }
|
|
}
|
|
// Nothing selected
|
|
if (!e.selected.length) {
|
|
if (this._autoClose) {
|
|
this.clear()
|
|
} else {
|
|
var deselectedFeatures = e.deselected
|
|
deselectedFeatures.forEach(deselectedFeature => {
|
|
var selectClusterFeatures = deselectedFeature.get('selectcluserfeatures')
|
|
if (selectClusterFeatures) {
|
|
selectClusterFeatures.forEach(selectClusterFeature => {
|
|
this.overlayLayer_.getSource().removeFeature(selectClusterFeature)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get selection
|
|
var feature = e.selected[0]
|
|
// It's one of ours
|
|
if (feature.get('selectclusterfeature'))
|
|
return
|
|
|
|
// Clic out of the cluster => close it
|
|
var source = this.overlayLayer_.getSource()
|
|
if (this._autoClose) {
|
|
source.clear()
|
|
}
|
|
|
|
var cluster = feature.get('features')
|
|
// Not a cluster (or just one feature)
|
|
if (!cluster || cluster.length == 1)
|
|
return
|
|
|
|
// Remove cluster from selection
|
|
if (!this.selectCluster_)
|
|
this.getFeatures().clear()
|
|
|
|
var center = feature.getGeometry().getCoordinates()
|
|
// Pixel size in map unit
|
|
var view = this.getMap().getView()
|
|
var userproj = ol_proj_getUserProjection()
|
|
var pix = view.getResolution() * (userproj ? view.getProjection().getMetersPerUnit() / userproj.getMetersPerUnit() : 1)
|
|
var r, a, i, max
|
|
var p, cf, lk
|
|
|
|
// The features
|
|
var features = []
|
|
|
|
// Draw on a circle
|
|
if (!this.spiral || cluster.length <= this.circleMaxObjects) {
|
|
max = Math.min(cluster.length, this.circleMaxObjects)
|
|
r = pix * this.pointRadius * (0.5 + max / 4)
|
|
for (i = 0; i < max; i++) {
|
|
a = 2 * Math.PI * i / max
|
|
if (max == 2 || max == 4)
|
|
a += Math.PI / 4
|
|
p = [center[0] + r * Math.sin(a), center[1] + r * Math.cos(a)]
|
|
cf = new ol_Feature({ 'selectclusterfeature': true, 'features': [cluster[i]], geometry: new ol_geom_Point(p) })
|
|
cf.setStyle(cluster[i].getStyle())
|
|
features.push(cf)
|
|
lk = new ol_Feature({ 'selectclusterlink': true, geometry: new ol_geom_LineString([center, p]) })
|
|
features.push(lk)
|
|
}
|
|
}
|
|
|
|
// Draw on a spiral
|
|
else {
|
|
// Start angle
|
|
a = 0
|
|
var d = 2 * this.pointRadius
|
|
max = Math.min(this.maxObjects, cluster.length)
|
|
// Feature on a spiral
|
|
for (i = 0; i < max; i++) {
|
|
// New radius => increase d in one turn
|
|
r = d / 2 + d * a / (2 * Math.PI)
|
|
// Angle
|
|
a = a + (d + 0.1) / r
|
|
var dx = pix * r * Math.sin(a)
|
|
var dy = pix * r * Math.cos(a)
|
|
p = [center[0] + dx, center[1] + dy]
|
|
cf = new ol_Feature({ 'selectclusterfeature': true, 'features': [cluster[i]], geometry: new ol_geom_Point(p) })
|
|
cf.setStyle(cluster[i].getStyle())
|
|
features.push(cf)
|
|
lk = new ol_Feature({ 'selectclusterlink': true, geometry: new ol_geom_LineString([center, p]) })
|
|
features.push(lk)
|
|
}
|
|
}
|
|
|
|
feature.set('selectcluserfeatures', features)
|
|
if (this.animate) {
|
|
this.animateCluster_(center, features)
|
|
} else {
|
|
source.addFeatures(features)
|
|
}
|
|
}
|
|
/**
|
|
* Animate the cluster and spread out the features
|
|
* @param {ol.Coordinates} the center of the cluster
|
|
*/
|
|
animateCluster_(center, features) {
|
|
// Stop animation (if one is running)
|
|
if (this.listenerKey_) {
|
|
ol_Observable_unByKey(this.listenerKey_)
|
|
}
|
|
|
|
// Features to animate
|
|
// var features = this.overlayLayer_.getSource().getFeatures();
|
|
if (!features.length)
|
|
return
|
|
|
|
var style = this.overlayLayer_.getStyle()
|
|
var stylefn = (typeof (style) == 'function') ? style : style.length ? function () { return style } : function () { return [style] }
|
|
var duration = this.animationDuration || 500
|
|
var start = new Date().getTime()
|
|
|
|
// Animate function
|
|
function animate(event) {
|
|
var vectorContext = event.vectorContext || ol_render_getVectorContext(event)
|
|
// Retina device
|
|
var ratio = event.frameState.pixelRatio
|
|
var view = this.getMap().getView()
|
|
var userproj = ol_proj_getUserProjection()
|
|
var res = view.getResolution() + (userproj ? view.getProjection().getMetersPerUnit() / userproj.getMetersPerUnit() : 1)
|
|
var e = ol_easing_easeOut((event.frameState.time - start) / duration)
|
|
for (var i = 0, feature; feature = features[i]; i++)
|
|
if (feature.get('features')) {
|
|
var pt = feature.getGeometry().getCoordinates()
|
|
pt[0] = center[0] + e * (pt[0] - center[0])
|
|
pt[1] = center[1] + e * (pt[1] - center[1])
|
|
var geo = new ol_geom_Point(pt)
|
|
// Image style
|
|
var st = stylefn(feature, res)
|
|
for (var s = 0; s < st.length; s++) {
|
|
var sc
|
|
// OL < v4.3 : setImageStyle doesn't check retina
|
|
var imgs = ol_Map.prototype.getFeaturesAtPixel ? false : st[s].getImage()
|
|
if (imgs) {
|
|
sc = imgs.getScale()
|
|
imgs.setScale(ratio)
|
|
}
|
|
// OL3 > v3.14
|
|
if (vectorContext.setStyle) {
|
|
vectorContext.setStyle(st[s])
|
|
vectorContext.drawGeometry(geo)
|
|
}
|
|
|
|
// older version
|
|
else {
|
|
vectorContext.setImageStyle(imgs)
|
|
vectorContext.drawPointGeometry(geo)
|
|
}
|
|
if (imgs)
|
|
imgs.setScale(sc)
|
|
}
|
|
}
|
|
// Stop animation and restore cluster visibility
|
|
if (e > 1.0) {
|
|
ol_Observable_unByKey(this.listenerKey_)
|
|
this.overlayLayer_.getSource().addFeatures(features)
|
|
this.overlayLayer_.changed()
|
|
return
|
|
}
|
|
|
|
|
|
// tell OL3 to continue postcompose animation
|
|
event.frameState.animate = true
|
|
}
|
|
|
|
// Start a new postcompose animation
|
|
this.listenerKey_ = this.overlayLayer_.on(['postcompose', 'postrender'], animate.bind(this))
|
|
// Start animation with a ghost feature
|
|
var feature = new ol_Feature(new ol_geom_Point(this.getMap().getView().getCenter()))
|
|
feature.setStyle(new ol_style_Style({ image: new ol_style_Circle({}) }))
|
|
this.overlayLayer_.getSource().addFeature(feature)
|
|
}
|
|
/** Helper function to get the extent of a cluster
|
|
* @param {ol.feature} feature
|
|
* @return {ol.extent|null} the extent or null if extent is empty (no cluster or superimposed points)
|
|
*/
|
|
getClusterExtent(feature) {
|
|
if (!feature.get('features'))
|
|
return null
|
|
var extent = ol_extent_createEmpty()
|
|
feature.get('features').forEach(function (f) {
|
|
extent = ol_extent_extend(extent, f.getGeometry().getExtent())
|
|
})
|
|
if (extent[0] === extent[2] && extent[1] === extent[3])
|
|
return null
|
|
return extent
|
|
}
|
|
}
|
|
|
|
export default ol_interaction_SelectCluster
|