| @@ -2,39 +2,52 @@ | |||
| * @Author : Caven Chen | |||
| */ | |||
| import Supercluster from 'supercluster' | |||
| import { Cesium } from '../../../namespace' | |||
| import State from '../../state/State' | |||
| import Layer from '../Layer' | |||
| import Parse from '../../parse/Parse' | |||
| const DEF_OPT = { | |||
| size: 18, | |||
| pixelRange: 40, | |||
| gradient: { | |||
| radius: 60, | |||
| maxZoom: 25, | |||
| style: 'circle', | |||
| image: '', | |||
| gradientColors: { | |||
| 0.0001: Cesium.Color.DEEPSKYBLUE, | |||
| 0.001: Cesium.Color.GREEN, | |||
| 0.01: Cesium.Color.ORANGE, | |||
| 0.1: Cesium.Color.RED, | |||
| }, | |||
| gradientImages: {}, | |||
| showCount: true, | |||
| fontSize: 12, | |||
| clusterSize: 16, | |||
| 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 { | |||
| constructor(id, options = {}) { | |||
| super(id) | |||
| this._delegate = new Cesium.CustomDataSource(id) | |||
| this._delegate = new Cesium.PrimitiveCollection() | |||
| this._options = { | |||
| ...DEF_OPT, | |||
| ...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 | |||
| } | |||
| @@ -42,20 +55,19 @@ class ClusterLayer extends Layer { | |||
| return Layer.getLayerType('cluster') | |||
| } | |||
| set enableCluster(enableCluster) { | |||
| this._delegate.clustering.enabled = enableCluster | |||
| } | |||
| _addOverlay(overlay) {} | |||
| _removeOverlay(overlay) {} | |||
| /** | |||
| * | |||
| * @param color | |||
| * @param numLength | |||
| * @param count | |||
| * @returns {*} | |||
| * @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]) { | |||
| let canvas = document.createElement('canvas') | |||
| canvas.width = size | |||
| @@ -71,8 +83,8 @@ class ClusterLayer extends Layer { | |||
| context2D.beginPath() | |||
| context2D.arc(12, 12, 6, 0, 2 * Math.PI) | |||
| context2D.fillStyle = color.toCssColorString() | |||
| context2D.fill() | |||
| context2D.closePath() | |||
| context2D.fill() | |||
| context2D.restore() | |||
| this._cache[key] = canvas.toDataURL() | |||
| } | |||
| @@ -82,13 +94,13 @@ class ClusterLayer extends Layer { | |||
| /** | |||
| * | |||
| * @param color | |||
| * @param numLength | |||
| * @param count | |||
| * @returns {*} | |||
| * @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 angle = Math.PI / 2 | |||
| let intervalAngle = Math.PI / 6 | |||
| @@ -122,56 +134,133 @@ class ClusterLayer extends Layer { | |||
| 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() { | |||
| this._delegate.entities.removeAll() | |||
| this._cache = {} | |||
| this._allCount = 0 | |||
| this._state = State.CLEARED | |||
| return this | |||
| } | |||