| * @Author : Caven Chen | * @Author : Caven Chen | ||||
| */ | */ | ||||
| import Supercluster from 'supercluster' | |||||
| import { Cesium } from '../../../namespace' | import { Cesium } from '../../../namespace' | ||||
| import State from '../../state/State' | import State from '../../state/State' | ||||
| import Layer from '../Layer' | import Layer from '../Layer' | ||||
| import Parse from '../../parse/Parse' | |||||
| const DEF_OPT = { | const DEF_OPT = { | ||||
| size: 18, | |||||
| pixelRange: 40, | |||||
| gradient: { | |||||
| radius: 60, | |||||
| maxZoom: 25, | |||||
| style: 'circle', | |||||
| image: '', | |||||
| gradientColors: { | |||||
| 0.0001: Cesium.Color.DEEPSKYBLUE, | 0.0001: Cesium.Color.DEEPSKYBLUE, | ||||
| 0.001: Cesium.Color.GREEN, | 0.001: Cesium.Color.GREEN, | ||||
| 0.01: Cesium.Color.ORANGE, | 0.01: Cesium.Color.ORANGE, | ||||
| 0.1: Cesium.Color.RED, | 0.1: Cesium.Color.RED, | ||||
| }, | }, | ||||
| gradientImages: {}, | |||||
| showCount: true, | |||||
| fontSize: 12, | fontSize: 12, | ||||
| clusterSize: 16, | |||||
| fontColor: Cesium.Color.BLACK, | fontColor: Cesium.Color.BLACK, | ||||
| style: 'circle', | |||||
| getCountOffset: (count) => { | |||||
| return { | |||||
| x: -3.542857 * String(count).length + 1.066667, | |||||
| y: String(count).length > 3 ? 5 : 4, | |||||
| } | |||||
| }, | |||||
| } | } | ||||
| class ClusterLayer extends Layer { | class ClusterLayer extends Layer { | ||||
| constructor(id, options = {}) { | constructor(id, options = {}) { | ||||
| super(id) | super(id) | ||||
| this._delegate = new Cesium.CustomDataSource(id) | |||||
| this._delegate = new Cesium.PrimitiveCollection() | |||||
| this._options = { | this._options = { | ||||
| ...DEF_OPT, | ...DEF_OPT, | ||||
| ...options, | ...options, | ||||
| } | } | ||||
| this._delegate.clustering.enabled = true | |||||
| this._delegate.clustering.clusterEvent.addEventListener( | |||||
| this._clusterEventHandler, | |||||
| this | |||||
| ) | |||||
| this._delegate.clustering.pixelRange = this._options.pixelRange | |||||
| this._billboards = this._delegate.add(new Cesium.BillboardCollection()) | |||||
| this._labels = this._delegate.add(new Cesium.LabelCollection()) | |||||
| this._cluster = new Supercluster({ | |||||
| radius: this._options.radius, | |||||
| maxZoom: this._options.maxZoom, | |||||
| }) | |||||
| this._allCount = 0 | |||||
| this._changedRemoveCallback = undefined | |||||
| this._state = State.INITIALIZED | this._state = State.INITIALIZED | ||||
| } | } | ||||
| return Layer.getLayerType('cluster') | return Layer.getLayerType('cluster') | ||||
| } | } | ||||
| set enableCluster(enableCluster) { | |||||
| this._delegate.clustering.enabled = enableCluster | |||||
| } | |||||
| _addOverlay(overlay) {} | |||||
| _removeOverlay(overlay) {} | |||||
| /** | /** | ||||
| * | * | ||||
| * @param color | * @param color | ||||
| * @param numLength | |||||
| * @param count | |||||
| * @returns {*} | * @returns {*} | ||||
| * @private | * @private | ||||
| */ | */ | ||||
| _drawCircle(color, numLength) { | |||||
| let size = this._options.size * (numLength + 1) | |||||
| let key = color.toCssColorString() + '-' + size | |||||
| _getCircleImage(color, count) { | |||||
| let size = this._options.clusterSize * (String(count).length + 1) | |||||
| let key = color.toCssColorString() + '-' + count | |||||
| if (!this._cache[key]) { | if (!this._cache[key]) { | ||||
| let canvas = document.createElement('canvas') | let canvas = document.createElement('canvas') | ||||
| canvas.width = size | canvas.width = size | ||||
| context2D.beginPath() | context2D.beginPath() | ||||
| context2D.arc(12, 12, 6, 0, 2 * Math.PI) | context2D.arc(12, 12, 6, 0, 2 * Math.PI) | ||||
| context2D.fillStyle = color.toCssColorString() | context2D.fillStyle = color.toCssColorString() | ||||
| context2D.fill() | |||||
| context2D.closePath() | context2D.closePath() | ||||
| context2D.fill() | |||||
| context2D.restore() | context2D.restore() | ||||
| this._cache[key] = canvas.toDataURL() | this._cache[key] = canvas.toDataURL() | ||||
| } | } | ||||
| /** | /** | ||||
| * | * | ||||
| * @param color | * @param color | ||||
| * @param numLength | |||||
| * @param count | |||||
| * @returns {*} | * @returns {*} | ||||
| * @private | * @private | ||||
| */ | */ | ||||
| _drawClustering(color, numLength) { | |||||
| let size = this._options.size * (numLength + 1) | |||||
| let key = color.toCssColorString() + '-' + size | |||||
| _getClusteringImage(color, count) { | |||||
| let size = this._options.clusterSize * (String(count).length + 1) | |||||
| let key = color.toCssColorString() + '-' + count | |||||
| let startAngle = -Math.PI / 12 | let startAngle = -Math.PI / 12 | ||||
| let angle = Math.PI / 2 | let angle = Math.PI / 2 | ||||
| let intervalAngle = Math.PI / 6 | let intervalAngle = Math.PI / 6 | ||||
| return this._cache[key] | return this._cache[key] | ||||
| } | } | ||||
| _getClusterImage(count) { | |||||
| let rate = count / this._allCount | |||||
| let image = undefined | |||||
| if (this._options.style === 'custom') { | |||||
| let keys = Object.keys(this._options.gradientImages).sort( | |||||
| (a, b) => Number(a) - Number(b) | |||||
| ) | |||||
| for (let i = keys.length - 1; i >= 0; i--) { | |||||
| if (rate >= Number(keys[i])) { | |||||
| image = this._options.gradientImages[keys[i]] | |||||
| break | |||||
| } | |||||
| } | |||||
| if (!image) { | |||||
| image = this._options.gradientImages[keys[0]] | |||||
| } | |||||
| } else { | |||||
| let keys = Object.keys(this._options.gradientColors).sort( | |||||
| (a, b) => Number(a) - Number(b) | |||||
| ) | |||||
| let color = undefined | |||||
| for (let i = keys.length - 1; i >= 0; i--) { | |||||
| if (rate >= Number(keys[i])) { | |||||
| color = this._options.gradientColors[keys[i]] | |||||
| break | |||||
| } | |||||
| } | |||||
| if (!color) { | |||||
| color = this._options.gradientColors[keys[0]] | |||||
| } | |||||
| image = | |||||
| this._options.style === 'circle' | |||||
| ? this._getCircleImage(color, count) | |||||
| : this._getClusteringImage(color, count) | |||||
| } | |||||
| return image | |||||
| } | |||||
| _changeCluster() { | |||||
| this._billboards.removeAll() | |||||
| this._labels.removeAll() | |||||
| let rectangle = this._viewer.camera.computeViewRectangle() | |||||
| if (this._allCount) { | |||||
| let result = this._cluster.getClusters( | |||||
| [ | |||||
| Cesium.Math.toDegrees(rectangle.west), | |||||
| Cesium.Math.toDegrees(rectangle.south), | |||||
| Cesium.Math.toDegrees(rectangle.east), | |||||
| Cesium.Math.toDegrees(rectangle.north), | |||||
| ], | |||||
| this._viewer.zoom | |||||
| ) | |||||
| result.forEach((item) => { | |||||
| if (item.properties) { | |||||
| let count = item.properties.point_count | |||||
| this._billboards.add({ | |||||
| position: Cesium.Cartesian3.fromDegrees( | |||||
| +item.geometry.coordinates[0], | |||||
| +item.geometry.coordinates[1] | |||||
| ), | |||||
| image: this._getClusterImage(count), | |||||
| }) | |||||
| if (this._options.showCount) { | |||||
| this._labels.add({ | |||||
| position: Cesium.Cartesian3.fromDegrees( | |||||
| +item.geometry.coordinates[0], | |||||
| +item.geometry.coordinates[1] | |||||
| ), | |||||
| text: String(count), | |||||
| font: `${this._options.fontSize} px sans-serif`, | |||||
| disableDepthTestDistance: Number.POSITIVE_INFINITY, | |||||
| fillColor: this._options.fontColor, | |||||
| scale: 0.8, | |||||
| pixelOffset: this._options.getCountOffset(count), | |||||
| }) | |||||
| } | |||||
| } else { | |||||
| this._billboards.add({ | |||||
| position: Cesium.Cartesian3.fromDegrees( | |||||
| +item.geometry.coordinates[0], | |||||
| +item.geometry.coordinates[1] | |||||
| ), | |||||
| image: this._options.image, | |||||
| }) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| _addedHook() { | |||||
| this._changedRemoveCallback = this._viewer.camera.changed.addEventListener( | |||||
| this._changeCluster, | |||||
| this | |||||
| ) | |||||
| } | |||||
| _removedHook() { | |||||
| this._changedRemoveCallback && this._changedRemoveCallback() | |||||
| } | |||||
| /** | /** | ||||
| * | * | ||||
| * @param {*} clusteredEntities | |||||
| * @param {*} cluster | |||||
| * @param points | |||||
| * @returns {ClusterLayer} | |||||
| */ | */ | ||||
| _clusterEventHandler(clusteredEntities, cluster) { | |||||
| if (!this._delegate.clustering.enabled) { | |||||
| return | |||||
| } | |||||
| cluster.billboard.show = true | |||||
| cluster.label.font = `bold ${this._options.fontSize}px sans-serif` | |||||
| cluster.label.fillColor = this._options.fontColor | |||||
| cluster.label.disableDepthTestDistance = Number.POSITIVE_INFINITY | |||||
| if (this._delegate.entities.values.length) { | |||||
| let allCount = this._delegate.entities.values.length || 0 | |||||
| for (let key in this._options.gradient) { | |||||
| if (clusteredEntities.length >= allCount * key) { | |||||
| let numLength = String(clusteredEntities.length).length | |||||
| if (this._options.style === 'circle') { | |||||
| cluster.billboard.image = this._drawCircle( | |||||
| this._options.gradient[key], | |||||
| numLength | |||||
| ) | |||||
| } else if (this._options.style === 'custom') { | |||||
| cluster.billboard.image = this._options.gradient[key] | |||||
| } else { | |||||
| cluster.billboard.image = this._drawClustering( | |||||
| this._options.gradient[key], | |||||
| numLength | |||||
| ) | |||||
| setPoints(points = []) { | |||||
| if (points.length) { | |||||
| this._allCount = points.length | |||||
| this._cluster.load( | |||||
| points.map((item) => { | |||||
| let position = Parse.parsePosition(item) | |||||
| return { | |||||
| type: 'Feature', | |||||
| geometry: { | |||||
| type: 'Point', | |||||
| coordinates: [position.lng, position.lat], | |||||
| }, | |||||
| } | } | ||||
| cluster.label.show = true | |||||
| if (numLength === 1) { | |||||
| cluster.label.pixelOffset = new Cesium.Cartesian2(-2, 3) | |||||
| } else { | |||||
| cluster.label.pixelOffset = new Cesium.Cartesian2( | |||||
| -5 * (numLength - 1), | |||||
| 5 | |||||
| ) | |||||
| } | |||||
| } else if (clusteredEntities.length <= 1) { | |||||
| cluster.label.show = false | |||||
| } | |||||
| } | |||||
| }) | |||||
| ) | |||||
| } | } | ||||
| return this | |||||
| } | } | ||||
| clear() { | clear() { | ||||
| this._delegate.entities.removeAll() | |||||
| this._cache = {} | this._cache = {} | ||||
| this._allCount = 0 | |||||
| this._state = State.CLEARED | this._state = State.CLEARED | ||||
| return this | return this | ||||
| } | } |