| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645 |
- import * as maptalks from 'maptalks'
- const options = {
- 'maxClusterRadius': 160,
- 'textSumProperty': null,
- 'symbol': null,
- 'drawClusterText': true,
- 'textSymbol': null,
- 'animation': true,
- 'animationDuration': 450,
- 'maxClusterZoom': null,
- 'noClusterWithOneMarker': true,
- 'forceRenderOnZooming': true
- }
- export class ClusterLayer extends maptalks.VectorLayer {
- /**
- * Reproduce a ClusterLayer from layer's profile JSON.
- * @param {Object} json - layer's profile JSON
- * @return {maptalks.ClusterLayer}
- * @static
- * @private
- * @function
- */
- static fromJSON(json) {
- if (!json || json['type'] !== 'ClusterLayer') { return null }
- const layer = new ClusterLayer(json['id'], json['options'])
- const geoJSONs = json['geometries']
- const geometries = []
- for (let i = 0; i < geoJSONs.length; i++) {
- const geo = maptalks.Geometry.fromJSON(geoJSONs[i])
- if (geo) {
- geometries.push(geo)
- }
- }
- layer.addGeometry(geometries)
- return layer
- }
- addMarker(markers) {
- return this.addGeometry(markers)
- }
- addGeometry(markers) {
- for (let i = 0, len = markers.length; i < len; i++) {
- if (!(markers[i] instanceof maptalks.Marker)) {
- throw new Error('Only a point(Marker) can be added into a ClusterLayer')
- }
- }
- return super.addGeometry.apply(this, arguments)
- }
- onConfig(conf) {
- super.onConfig(conf)
- if (conf['maxClusterRadius'] ||
- conf['symbol'] ||
- conf['drawClusterText'] ||
- conf['textSymbol'] ||
- conf['maxClusterZoom']) {
- const renderer = this._getRenderer()
- if (renderer) {
- renderer.render()
- }
- }
- return this
- }
- /**
- * Identify the clusters on the given coordinate
- * @param {maptalks.Coordinate} coordinate - coordinate to identify
- * @return {Object|Geometry[]} result: cluster { center : [cluster's center], children : [geometries in the cluster] } or markers
- */
- identify(coordinate, options) {
- const map = this.getMap()
- const maxZoom = this.options['maxClusterZoom']
- if (maxZoom && map && map.getZoom() > maxZoom) {
- return super.identify(coordinate, options)
- }
- if (this._getRenderer()) {
- return this._getRenderer().identify(coordinate, options)
- }
- return null
- }
- /**
- * Export the ClusterLayer's JSON.
- * @return {Object} layer's JSON
- */
- toJSON() {
- const json = super.toJSON.call(this)
- json['type'] = 'ClusterLayer'
- return json
- }
- /**
- * Get the ClusterLayer's current clusters
- * @return {Object} layer's clusters
- **/
- getClusters() {
- const renderer = this._getRenderer()
- if (renderer) {
- return renderer._currentClusters || []
- }
- return []
- }
- }
- // merge to define ClusterLayer's default options.
- ClusterLayer.mergeOptions(options)
- // register ClusterLayer's JSON type for JSON deserialization.
- ClusterLayer.registerJSONType('ClusterLayer')
- const defaultTextSymbol = {
- 'textFaceName': '"microsoft yahei"',
- 'textSize': 16,
- 'textDx': 0,
- 'textDy': 0
- }
- const defaultSymbol = {
- 'markerType': 'ellipse',
- // 'markerFill': { property: 'count', type: 'interval', stops: [[0, 'rgb(135, 196, 240)'], [9, '#1bbc9b'], [99, 'rgb(216, 115, 149)']] },
- 'markerFill': { property: 'count', type: 'interval', stops: [[0, '#1bbc9b'], [9, '#1bbc9b'], [99, '#1bbc9b']] },
- 'markerFillOpacity': 0.7,
- 'markerLineOpacity': 1,
- 'markerLineWidth': 3,
- 'markerLineColor': '#fff',
- 'markerWidth': { property: 'count', type: 'interval', stops: [[0, 40], [9, 60], [99, 80]] },
- 'markerHeight': { property: 'count', type: 'interval', stops: [[0, 40], [9, 60], [99, 80]] }
- }
- ClusterLayer.registerRenderer('canvas', class extends maptalks.renderer.VectorLayerCanvasRenderer {
- constructor(layer) {
- super(layer)
- this._animated = true
- this._refreshStyle()
- this._clusterNeedRedraw = true
- }
- checkResources() {
- const symbol = this.layer.options['symbol'] || defaultSymbol
- const resources = super.checkResources.apply(this, arguments)
- if (symbol !== this._symbolResourceChecked) {
- const res = maptalks.Util.getExternalResources(symbol, true)
- if (res) {
- resources.push.apply(resources, res)
- }
- this._symbolResourceChecked = symbol
- }
- return resources
- }
- draw() {
- if (!this.canvas) {
- this.prepareCanvas()
- }
- const map = this.getMap()
- const zoom = map.getZoom()
- const maxClusterZoom = this.layer.options['maxClusterZoom']
- if (maxClusterZoom && zoom > maxClusterZoom) {
- delete this._currentClusters
- this._markersToDraw = this.layer._geoList
- super.draw.apply(this, arguments)
- return
- }
- if (this._clusterNeedRedraw) {
- this._clearDataCache()
- this._computeGrid()
- this._clusterNeedRedraw = false
- }
- const zoomClusters = this._clusterCache[zoom] ? this._clusterCache[zoom]['clusters'] : null
- const clusters = this._getClustersToDraw(zoomClusters)
- clusters.zoom = zoom
- this._drawLayer(clusters)
- }
- _getClustersToDraw(zoomClusters) {
- this._markersToDraw = []
- const map = this.getMap()
- const font = maptalks.StringUtil.getFont(this._textSymbol)
- const digitLen = maptalks.StringUtil.stringLength('9', font).toPoint()
- const extent = map.getContainerExtent()
- const clusters = []
- let pt, pExt, sprite, width, height
- for (const p in zoomClusters) {
- this._currentGrid = zoomClusters[p]
- if (zoomClusters[p]['count'] === 1 && this.layer.options['noClusterWithOneMarker']) {
- const marker = zoomClusters[p]['children'][0]
- marker._cluster = zoomClusters[p]
- this._markersToDraw.push(marker)
- continue
- }
- sprite = this._getSprite()
- width = sprite.canvas.width
- height = sprite.canvas.height
- pt = map._prjToContainerPoint(zoomClusters[p]['center'])
- pExt = new maptalks.PointExtent(pt.sub(width, height), pt.add(width, height))
- if (!extent.intersects(pExt)) {
- continue
- }
- if (!zoomClusters[p]['textSize']) {
- const text = this._getClusterText(zoomClusters[p])
- zoomClusters[p]['textSize'] = new maptalks.Point(digitLen.x * text.length, digitLen.y)._multi(1 / 2)
- }
- clusters.push(zoomClusters[p])
- }
- return clusters
- }
- drawOnInteracting() {
- if (this._currentClusters) {
- this._drawClusters(this._currentClusters, 1)
- }
- super.drawOnInteracting.apply(this, arguments)
- }
- _getCurrentNeedRenderGeos() {
- if (this._markersToDraw) {
- return this._markersToDraw
- }
- return []
- }
- forEachGeo(fn, context) {
- if (this._markersToDraw) {
- this._markersToDraw.forEach((g) => {
- if (context) {
- fn.call(context, g)
- } else {
- fn(g)
- }
- })
- }
- }
- onGeometryShow() {
- this._clusterNeedRedraw = true
- super.onGeometryShow.apply(this, arguments)
- }
- onGeometryHide() {
- this._clusterNeedRedraw = true
- super.onGeometryHide.apply(this, arguments)
- }
- onGeometryAdd() {
- this._clusterNeedRedraw = true
- super.onGeometryAdd.apply(this, arguments)
- }
- onGeometryRemove() {
- this._clusterNeedRedraw = true
- super.onGeometryRemove.apply(this, arguments)
- }
- onGeometryPositionChange() {
- this._clusterNeedRedraw = true
- super.onGeometryPositionChange.apply(this, arguments)
- }
- onRemove() {
- this._clearDataCache()
- }
- identify(coordinate, options) {
- const map = this.getMap()
- const maxZoom = this.layer.options['maxClusterZoom']
- if (maxZoom && map.getZoom() > maxZoom) {
- return super.identify(coordinate, options)
- }
- if (this._currentClusters) {
- const point = map.coordinateToContainerPoint(coordinate)
- const old = this._currentGrid
- for (let i = 0; i < this._currentClusters.length; i++) {
- const c = this._currentClusters[i]
- const pt = map._prjToContainerPoint(c['center'])
- this._currentGrid = c
- const markerWidth = this._getSprite().canvas.width
- if (point.distanceTo(pt) <= markerWidth) {
- return {
- 'center': map.getProjection().unproject(c.center.copy()),
- 'children': c.children.slice(0)
- }
- }
- }
- this._currentGrid = old
- }
- // if no clusters is hit, identify markers
- if (this._markersToDraw && this._markersToDraw[0]) {
- const point = map.coordinateToContainerPoint(coordinate)
- return this.layer._hitGeos(this._markersToDraw, point, options)
- }
- return null
- }
- onSymbolChanged() {
- this._refreshStyle()
- this._computeGrid()
- this._stopAnim()
- this.setToRedraw()
- }
- _refreshStyle() {
- const symbol = this.layer.options['symbol'] || defaultSymbol
- const textSymbol = this.layer.options['textSymbol'] || defaultTextSymbol
- const argFn = () => [this.getMap().getZoom(), this._currentGrid]
- this._symbol = maptalks.MapboxUtil.loadFunctionTypes(symbol, argFn)
- this._textSymbol = maptalks.MapboxUtil.loadFunctionTypes(textSymbol, argFn)
- }
- _drawLayer(clusters) {
- const parentClusters = this._currentClusters || clusters
- this._currentClusters = clusters
- delete this._clusterMaskExtent
- const layer = this.layer
- //if (layer.options['animation'] && this._animated && this._inout === 'out') {
- if (layer.options['animation'] && this._animated && this._inout) {
- let dr = [0, 1]
- if (this._inout === 'in') {
- dr = [1, 0]
- }
- this._player = maptalks.animation.Animation.animate(
- { 'd': dr },
- { 'speed': layer.options['animationDuration'], 'easing': 'inAndOut' },
- frame => {
- if (frame.state.playState === 'finished') {
- this._animated = false
- this._drawClusters(clusters, 1)
- this._drawMarkers()
- this.completeRender()
- } else {
- if (this._inout === 'in') {
- this._drawClustersFrame(clusters, parentClusters, frame.styles.d)
- } else {
- this._drawClustersFrame(parentClusters, clusters, frame.styles.d)
- }
- this.setCanvasUpdated()
- }
- }
- )
- .play()
- } else {
- this._animated = false
- this._drawClusters(clusters, 1)
- this._drawMarkers()
- this.completeRender()
- }
- }
- _drawMarkers() {
- super.drawGeos(this._clusterMaskExtent)
- }
- _drawClustersFrame(parentClusters, toClusters, ratio) {
- this._clusterMaskExtent = this.prepareCanvas()
- const map = this.getMap()
- const drawn = {}
- if (parentClusters) {
- parentClusters.forEach(c => {
- const p = map._prjToContainerPoint(c['center'])
- if (!drawn[c.key]) {
- drawn[c.key] = 1
- this._drawCluster(p, c, 1 - ratio)
- }
- })
- }
- if (ratio === 0 || !toClusters) {
- return
- }
- const z = parentClusters.zoom
- const r = map._getResolution(z) * this.layer.options['maxClusterRadius']
- const min = this._markerExtent.getMin()
- toClusters.forEach(c => {
- let pt = map._prjToContainerPoint(c['center'])
- const center = c.center
- const pgx = Math.floor((center.x - min.x) / r)
- const pgy = Math.floor((center.y - min.y) / r)
- const pkey = pgx + '_' + pgy
- const parent = this._clusterCache[z] ? this._clusterCache[z]['clusterMap'][pkey] : null
- if (parent) {
- const pp = map._prjToContainerPoint(parent['center'])
- pt = pp.add(pt.sub(pp)._multi(ratio))
- }
- this._drawCluster(pt, c, ratio > 0.5 ? 1 : ratio)
- })
- }
- _drawClusters(clusters, ratio) {
- if (!clusters) {
- return
- }
- this._clusterMaskExtent = this.prepareCanvas()
- const map = this.getMap()
- clusters.forEach(c => {
- const pt = map._prjToContainerPoint(c['center'])
- this._drawCluster(pt, c, ratio > 0.5 ? 1 : ratio)
- })
- }
- _drawCluster(pt, cluster, op) {
- this._currentGrid = cluster
- const ctx = this.context
- const sprite = this._getSprite()
- const opacity = ctx.globalAlpha
- if (opacity * op === 0) {
- return
- }
- ctx.globalAlpha = opacity * op
- if (sprite) {
- const pos = pt.add(sprite.offset)._sub(sprite.canvas.width / 2, sprite.canvas.height / 2)
- ctx.drawImage(sprite.canvas, pos.x, pos.y)
- }
- if (this.layer.options['drawClusterText'] && cluster['textSize']) {
- maptalks.Canvas.prepareCanvasFont(ctx, this._textSymbol)
- ctx.textBaseline = 'middle'
- const dx = this._textSymbol['textDx'] || 0
- const dy = this._textSymbol['textDy'] || 0
- const text = this._getClusterText(cluster)
- maptalks.Canvas.fillText(ctx, text, pt.sub(cluster['textSize'].x, 0)._add(dx, dy))
- }
- ctx.globalAlpha = opacity
- }
- _getClusterText(cluster) {
- const text = this.layer.options['textSumProperty'] ? cluster['textSumProperty'] : cluster['count']
- return text + ''
- }
- _getSprite() {
- if (!this._spriteCache) {
- this._spriteCache = {}
- }
- const key = maptalks.Util.getSymbolStamp(this._symbol)
- if (!this._spriteCache[key]) {
- this._spriteCache[key] = new maptalks.Marker([0, 0], { 'symbol': this._symbol })._getSprite(this.resources, this.getMap().CanvasClass)
- }
- return this._spriteCache[key]
- }
- _initGridSystem() {
- const points = []
- let extent, c
- this.layer.forEach(g => {
- if (!g.isVisible()) {
- return
- }
- c = g._getPrjCoordinates()
- if (!extent) {
- extent = g._getPrjExtent()
- } else {
- extent = extent._combine(g._getPrjExtent())
- }
- points.push({
- x: c.x,
- y: c.y,
- id: g._getInternalId(),
- geometry: g
- })
- })
- this._markerExtent = extent
- this._markerPoints = points
- }
- _computeGrid() {
- const map = this.getMap()
- const zoom = map.getZoom()
- if (!this._markerExtent) {
- this._initGridSystem()
- }
- if (!this._clusterCache) {
- this._clusterCache = {}
- }
- const pre = map._getResolution(map.getMinZoom()) > map._getResolution(map.getMaxZoom()) ? zoom - 1 : zoom + 1
- if (this._clusterCache[pre] && this._clusterCache[pre].length === this.layer.getCount()) {
- this._clusterCache[zoom] = this._clusterCache[pre]
- }
- if (!this._clusterCache[zoom]) {
- this._clusterCache[zoom] = this._computeZoomGrid(zoom)
- }
- map.fire('cluster-compute-grid')
- }
- _computeZoomGrid(zoom) {
- if (!this._markerExtent) {
- return null
- }
- const map = this.getMap()
- const r = map._getResolution(zoom) * this.layer.options['maxClusterRadius']
- const preT = map._getResolution(zoom - 1) ? map._getResolution(zoom - 1) * this.layer.options['maxClusterRadius'] : null
- let preCache = this._clusterCache[zoom - 1]
- if (!preCache && zoom - 1 >= map.getMinZoom()) {
- this._clusterCache[zoom - 1] = preCache = this._computeZoomGrid(zoom - 1)
- }
- // 1. format extent of markers to grids with raidus of r
- // 2. find point's grid in the grids
- // 3. sum up the point into the grid's collection
- const points = this._markerPoints
- const sumProperty = this.layer.options['textSumProperty']
- const grids = {}
- const min = this._markerExtent.getMin()
- let gx, gy, key, pgx, pgy, pkey
- for (let i = 0, len = points.length; i < len; i++) {
- const geo = points[i].geometry
- let sumProp = 0
- if (sumProperty && geo.getProperties() && geo.getProperties()[sumProperty]) {
- sumProp = geo.getProperties()[sumProperty]
- }
- gx = Math.floor((points[i].x - min.x) / r)
- gy = Math.floor((points[i].y - min.y) / r)
- key = gx + '_' + gy
- if (!grids[key]) {
- grids[key] = {
- 'sum': new maptalks.Coordinate(points[i].x, points[i].y),
- 'center': new maptalks.Coordinate(points[i].x, points[i].y),
- 'count': 1,
- 'textSumProperty': sumProp,
- 'children': [geo],
- 'key': key + ''
- }
- if (preT && preCache) {
- pgx = Math.floor((points[i].x - min.x) / preT)
- pgy = Math.floor((points[i].y - min.y) / preT)
- pkey = pgx + '_' + pgy
- grids[key]['parent'] = preCache['clusterMap'][pkey]
- }
- } else {
- grids[key]['sum']._add(new maptalks.Coordinate(points[i].x, points[i].y))
- grids[key]['count']++
- grids[key]['center'] = grids[key]['sum'].multi(1 / grids[key]['count'])
- grids[key]['children'].push(geo)
- grids[key]['textSumProperty'] += sumProp
- }
- }
- return this._mergeClusters(grids, r / 2)
- }
- _mergeClusters(grids, r) {
- const clusterMap = {}
- for (const p in grids) {
- clusterMap[p] = grids[p]
- }
- // merge adjacent clusters
- const merging = {}
- const visited = {}
- // find clusters need to merge
- let c1, c2
- for (const p in grids) {
- c1 = grids[p]
- if (visited[c1.key]) {
- continue
- }
- const gxgy = c1.key.split('_')
- const gx = +(gxgy[0])
- const gy = +(gxgy[1])
- //traverse adjacent grids
- for (let ii = -1; ii <= 1; ii++) {
- for (let iii = -1; iii <= 1; iii++) {
- if (ii === 0 && iii === 0) {
- continue
- }
- const key2 = (gx + ii) + '_' + (gy + iii)
- c2 = grids[key2]
- if (c2 && this._distanceTo(c1['center'], c2['center']) <= r) {
- if (!merging[c1.key]) {
- merging[c1.key] = []
- }
- merging[c1.key].push(c2)
- visited[c2.key] = 1
- }
- }
- }
- }
- //merge clusters
- for (const m in merging) {
- const grid = grids[m]
- if (!grid) {
- continue
- }
- const toMerge = merging[m]
- for (let i = 0; i < toMerge.length; i++) {
- if (grids[toMerge[i].key]) {
- grid['sum']._add(toMerge[i].sum)
- grid['count'] += toMerge[i].count
- grid['textSumProperty'] += toMerge[i].textSumProperty
- grid['children'] = grid['children'].concat(toMerge[i].children)
- clusterMap[toMerge[i].key] = grid
- delete grids[toMerge[i].key]
- }
- }
- grid['center'] = grid['sum'].multi(1 / grid['count'])
- }
- return {
- 'clusters': grids,
- 'clusterMap': clusterMap
- }
- }
- _distanceTo(c1, c2) {
- const x = c1.x - c2.x
- const y = c1.y - c2.y
- return Math.sqrt(x * x + y * y)
- }
- _stopAnim() {
- if (this._player && this._player.playState !== 'finished') {
- this._player.finish()
- }
- }
- onZoomStart(param) {
- this._stopAnim()
- super.onZoomStart(param)
- }
- onZoomEnd(param) {
- if (this.layer.isEmpty() || !this.layer.isVisible()) {
- super.onZoomEnd.apply(this, arguments)
- return
- }
- this._inout = param['from'] > param['to'] ? 'in' : 'out'
- this._animated = true
- this._computeGrid()
- super.onZoomEnd.apply(this, arguments)
- }
- _clearDataCache() {
- this._stopAnim()
- delete this._markerExtent
- delete this._markerPoints
- delete this._clusterCache
- delete this._zoomInClusters
- }
- })
|