MyClusterLayer.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. import * as maptalks from 'maptalks'
  2. const options = {
  3. 'maxClusterRadius': 160,
  4. 'textSumProperty': null,
  5. 'symbol': null,
  6. 'drawClusterText': true,
  7. 'textSymbol': null,
  8. 'animation': true,
  9. 'animationDuration': 450,
  10. 'maxClusterZoom': null,
  11. 'noClusterWithOneMarker': true,
  12. 'forceRenderOnZooming': true
  13. }
  14. export class ClusterLayer extends maptalks.VectorLayer {
  15. /**
  16. * Reproduce a ClusterLayer from layer's profile JSON.
  17. * @param {Object} json - layer's profile JSON
  18. * @return {maptalks.ClusterLayer}
  19. * @static
  20. * @private
  21. * @function
  22. */
  23. static fromJSON(json) {
  24. if (!json || json['type'] !== 'ClusterLayer') { return null }
  25. const layer = new ClusterLayer(json['id'], json['options'])
  26. const geoJSONs = json['geometries']
  27. const geometries = []
  28. for (let i = 0; i < geoJSONs.length; i++) {
  29. const geo = maptalks.Geometry.fromJSON(geoJSONs[i])
  30. if (geo) {
  31. geometries.push(geo)
  32. }
  33. }
  34. layer.addGeometry(geometries)
  35. return layer
  36. }
  37. addMarker(markers) {
  38. return this.addGeometry(markers)
  39. }
  40. addGeometry(markers) {
  41. for (let i = 0, len = markers.length; i < len; i++) {
  42. if (!(markers[i] instanceof maptalks.Marker)) {
  43. throw new Error('Only a point(Marker) can be added into a ClusterLayer')
  44. }
  45. }
  46. return super.addGeometry.apply(this, arguments)
  47. }
  48. onConfig(conf) {
  49. super.onConfig(conf)
  50. if (conf['maxClusterRadius'] ||
  51. conf['symbol'] ||
  52. conf['drawClusterText'] ||
  53. conf['textSymbol'] ||
  54. conf['maxClusterZoom']) {
  55. const renderer = this._getRenderer()
  56. if (renderer) {
  57. renderer.render()
  58. }
  59. }
  60. return this
  61. }
  62. /**
  63. * Identify the clusters on the given coordinate
  64. * @param {maptalks.Coordinate} coordinate - coordinate to identify
  65. * @return {Object|Geometry[]} result: cluster { center : [cluster's center], children : [geometries in the cluster] } or markers
  66. */
  67. identify(coordinate, options) {
  68. const map = this.getMap()
  69. const maxZoom = this.options['maxClusterZoom']
  70. if (maxZoom && map && map.getZoom() > maxZoom) {
  71. return super.identify(coordinate, options)
  72. }
  73. if (this._getRenderer()) {
  74. return this._getRenderer().identify(coordinate, options)
  75. }
  76. return null
  77. }
  78. /**
  79. * Export the ClusterLayer's JSON.
  80. * @return {Object} layer's JSON
  81. */
  82. toJSON() {
  83. const json = super.toJSON.call(this)
  84. json['type'] = 'ClusterLayer'
  85. return json
  86. }
  87. /**
  88. * Get the ClusterLayer's current clusters
  89. * @return {Object} layer's clusters
  90. **/
  91. getClusters() {
  92. const renderer = this._getRenderer()
  93. if (renderer) {
  94. return renderer._currentClusters || []
  95. }
  96. return []
  97. }
  98. }
  99. // merge to define ClusterLayer's default options.
  100. ClusterLayer.mergeOptions(options)
  101. // register ClusterLayer's JSON type for JSON deserialization.
  102. ClusterLayer.registerJSONType('ClusterLayer')
  103. const defaultTextSymbol = {
  104. 'textFaceName': '"microsoft yahei"',
  105. 'textSize': 16,
  106. 'textDx': 0,
  107. 'textDy': 0
  108. }
  109. const defaultSymbol = {
  110. 'markerType': 'ellipse',
  111. // 'markerFill': { property: 'count', type: 'interval', stops: [[0, 'rgb(135, 196, 240)'], [9, '#1bbc9b'], [99, 'rgb(216, 115, 149)']] },
  112. 'markerFill': { property: 'count', type: 'interval', stops: [[0, '#1bbc9b'], [9, '#1bbc9b'], [99, '#1bbc9b']] },
  113. 'markerFillOpacity': 0.7,
  114. 'markerLineOpacity': 1,
  115. 'markerLineWidth': 3,
  116. 'markerLineColor': '#fff',
  117. 'markerWidth': { property: 'count', type: 'interval', stops: [[0, 40], [9, 60], [99, 80]] },
  118. 'markerHeight': { property: 'count', type: 'interval', stops: [[0, 40], [9, 60], [99, 80]] }
  119. }
  120. ClusterLayer.registerRenderer('canvas', class extends maptalks.renderer.VectorLayerCanvasRenderer {
  121. constructor(layer) {
  122. super(layer)
  123. this._animated = true
  124. this._refreshStyle()
  125. this._clusterNeedRedraw = true
  126. }
  127. checkResources() {
  128. const symbol = this.layer.options['symbol'] || defaultSymbol
  129. const resources = super.checkResources.apply(this, arguments)
  130. if (symbol !== this._symbolResourceChecked) {
  131. const res = maptalks.Util.getExternalResources(symbol, true)
  132. if (res) {
  133. resources.push.apply(resources, res)
  134. }
  135. this._symbolResourceChecked = symbol
  136. }
  137. return resources
  138. }
  139. draw() {
  140. if (!this.canvas) {
  141. this.prepareCanvas()
  142. }
  143. const map = this.getMap()
  144. const zoom = map.getZoom()
  145. const maxClusterZoom = this.layer.options['maxClusterZoom']
  146. if (maxClusterZoom && zoom > maxClusterZoom) {
  147. delete this._currentClusters
  148. this._markersToDraw = this.layer._geoList
  149. super.draw.apply(this, arguments)
  150. return
  151. }
  152. if (this._clusterNeedRedraw) {
  153. this._clearDataCache()
  154. this._computeGrid()
  155. this._clusterNeedRedraw = false
  156. }
  157. const zoomClusters = this._clusterCache[zoom] ? this._clusterCache[zoom]['clusters'] : null
  158. const clusters = this._getClustersToDraw(zoomClusters)
  159. clusters.zoom = zoom
  160. this._drawLayer(clusters)
  161. }
  162. _getClustersToDraw(zoomClusters) {
  163. this._markersToDraw = []
  164. const map = this.getMap()
  165. const font = maptalks.StringUtil.getFont(this._textSymbol)
  166. const digitLen = maptalks.StringUtil.stringLength('9', font).toPoint()
  167. const extent = map.getContainerExtent()
  168. const clusters = []
  169. let pt, pExt, sprite, width, height
  170. for (const p in zoomClusters) {
  171. this._currentGrid = zoomClusters[p]
  172. if (zoomClusters[p]['count'] === 1 && this.layer.options['noClusterWithOneMarker']) {
  173. const marker = zoomClusters[p]['children'][0]
  174. marker._cluster = zoomClusters[p]
  175. this._markersToDraw.push(marker)
  176. continue
  177. }
  178. sprite = this._getSprite()
  179. width = sprite.canvas.width
  180. height = sprite.canvas.height
  181. pt = map._prjToContainerPoint(zoomClusters[p]['center'])
  182. pExt = new maptalks.PointExtent(pt.sub(width, height), pt.add(width, height))
  183. if (!extent.intersects(pExt)) {
  184. continue
  185. }
  186. if (!zoomClusters[p]['textSize']) {
  187. const text = this._getClusterText(zoomClusters[p])
  188. zoomClusters[p]['textSize'] = new maptalks.Point(digitLen.x * text.length, digitLen.y)._multi(1 / 2)
  189. }
  190. clusters.push(zoomClusters[p])
  191. }
  192. return clusters
  193. }
  194. drawOnInteracting() {
  195. if (this._currentClusters) {
  196. this._drawClusters(this._currentClusters, 1)
  197. }
  198. super.drawOnInteracting.apply(this, arguments)
  199. }
  200. _getCurrentNeedRenderGeos() {
  201. if (this._markersToDraw) {
  202. return this._markersToDraw
  203. }
  204. return []
  205. }
  206. forEachGeo(fn, context) {
  207. if (this._markersToDraw) {
  208. this._markersToDraw.forEach((g) => {
  209. if (context) {
  210. fn.call(context, g)
  211. } else {
  212. fn(g)
  213. }
  214. })
  215. }
  216. }
  217. onGeometryShow() {
  218. this._clusterNeedRedraw = true
  219. super.onGeometryShow.apply(this, arguments)
  220. }
  221. onGeometryHide() {
  222. this._clusterNeedRedraw = true
  223. super.onGeometryHide.apply(this, arguments)
  224. }
  225. onGeometryAdd() {
  226. this._clusterNeedRedraw = true
  227. super.onGeometryAdd.apply(this, arguments)
  228. }
  229. onGeometryRemove() {
  230. this._clusterNeedRedraw = true
  231. super.onGeometryRemove.apply(this, arguments)
  232. }
  233. onGeometryPositionChange() {
  234. this._clusterNeedRedraw = true
  235. super.onGeometryPositionChange.apply(this, arguments)
  236. }
  237. onRemove() {
  238. this._clearDataCache()
  239. }
  240. identify(coordinate, options) {
  241. const map = this.getMap()
  242. const maxZoom = this.layer.options['maxClusterZoom']
  243. if (maxZoom && map.getZoom() > maxZoom) {
  244. return super.identify(coordinate, options)
  245. }
  246. if (this._currentClusters) {
  247. const point = map.coordinateToContainerPoint(coordinate)
  248. const old = this._currentGrid
  249. for (let i = 0; i < this._currentClusters.length; i++) {
  250. const c = this._currentClusters[i]
  251. const pt = map._prjToContainerPoint(c['center'])
  252. this._currentGrid = c
  253. const markerWidth = this._getSprite().canvas.width
  254. if (point.distanceTo(pt) <= markerWidth) {
  255. return {
  256. 'center': map.getProjection().unproject(c.center.copy()),
  257. 'children': c.children.slice(0)
  258. }
  259. }
  260. }
  261. this._currentGrid = old
  262. }
  263. // if no clusters is hit, identify markers
  264. if (this._markersToDraw && this._markersToDraw[0]) {
  265. const point = map.coordinateToContainerPoint(coordinate)
  266. return this.layer._hitGeos(this._markersToDraw, point, options)
  267. }
  268. return null
  269. }
  270. onSymbolChanged() {
  271. this._refreshStyle()
  272. this._computeGrid()
  273. this._stopAnim()
  274. this.setToRedraw()
  275. }
  276. _refreshStyle() {
  277. const symbol = this.layer.options['symbol'] || defaultSymbol
  278. const textSymbol = this.layer.options['textSymbol'] || defaultTextSymbol
  279. const argFn = () => [this.getMap().getZoom(), this._currentGrid]
  280. this._symbol = maptalks.MapboxUtil.loadFunctionTypes(symbol, argFn)
  281. this._textSymbol = maptalks.MapboxUtil.loadFunctionTypes(textSymbol, argFn)
  282. }
  283. _drawLayer(clusters) {
  284. const parentClusters = this._currentClusters || clusters
  285. this._currentClusters = clusters
  286. delete this._clusterMaskExtent
  287. const layer = this.layer
  288. //if (layer.options['animation'] && this._animated && this._inout === 'out') {
  289. if (layer.options['animation'] && this._animated && this._inout) {
  290. let dr = [0, 1]
  291. if (this._inout === 'in') {
  292. dr = [1, 0]
  293. }
  294. this._player = maptalks.animation.Animation.animate(
  295. { 'd': dr },
  296. { 'speed': layer.options['animationDuration'], 'easing': 'inAndOut' },
  297. frame => {
  298. if (frame.state.playState === 'finished') {
  299. this._animated = false
  300. this._drawClusters(clusters, 1)
  301. this._drawMarkers()
  302. this.completeRender()
  303. } else {
  304. if (this._inout === 'in') {
  305. this._drawClustersFrame(clusters, parentClusters, frame.styles.d)
  306. } else {
  307. this._drawClustersFrame(parentClusters, clusters, frame.styles.d)
  308. }
  309. this.setCanvasUpdated()
  310. }
  311. }
  312. )
  313. .play()
  314. } else {
  315. this._animated = false
  316. this._drawClusters(clusters, 1)
  317. this._drawMarkers()
  318. this.completeRender()
  319. }
  320. }
  321. _drawMarkers() {
  322. super.drawGeos(this._clusterMaskExtent)
  323. }
  324. _drawClustersFrame(parentClusters, toClusters, ratio) {
  325. this._clusterMaskExtent = this.prepareCanvas()
  326. const map = this.getMap()
  327. const drawn = {}
  328. if (parentClusters) {
  329. parentClusters.forEach(c => {
  330. const p = map._prjToContainerPoint(c['center'])
  331. if (!drawn[c.key]) {
  332. drawn[c.key] = 1
  333. this._drawCluster(p, c, 1 - ratio)
  334. }
  335. })
  336. }
  337. if (ratio === 0 || !toClusters) {
  338. return
  339. }
  340. const z = parentClusters.zoom
  341. const r = map._getResolution(z) * this.layer.options['maxClusterRadius']
  342. const min = this._markerExtent.getMin()
  343. toClusters.forEach(c => {
  344. let pt = map._prjToContainerPoint(c['center'])
  345. const center = c.center
  346. const pgx = Math.floor((center.x - min.x) / r)
  347. const pgy = Math.floor((center.y - min.y) / r)
  348. const pkey = pgx + '_' + pgy
  349. const parent = this._clusterCache[z] ? this._clusterCache[z]['clusterMap'][pkey] : null
  350. if (parent) {
  351. const pp = map._prjToContainerPoint(parent['center'])
  352. pt = pp.add(pt.sub(pp)._multi(ratio))
  353. }
  354. this._drawCluster(pt, c, ratio > 0.5 ? 1 : ratio)
  355. })
  356. }
  357. _drawClusters(clusters, ratio) {
  358. if (!clusters) {
  359. return
  360. }
  361. this._clusterMaskExtent = this.prepareCanvas()
  362. const map = this.getMap()
  363. clusters.forEach(c => {
  364. const pt = map._prjToContainerPoint(c['center'])
  365. this._drawCluster(pt, c, ratio > 0.5 ? 1 : ratio)
  366. })
  367. }
  368. _drawCluster(pt, cluster, op) {
  369. this._currentGrid = cluster
  370. const ctx = this.context
  371. const sprite = this._getSprite()
  372. const opacity = ctx.globalAlpha
  373. if (opacity * op === 0) {
  374. return
  375. }
  376. ctx.globalAlpha = opacity * op
  377. if (sprite) {
  378. const pos = pt.add(sprite.offset)._sub(sprite.canvas.width / 2, sprite.canvas.height / 2)
  379. ctx.drawImage(sprite.canvas, pos.x, pos.y)
  380. }
  381. if (this.layer.options['drawClusterText'] && cluster['textSize']) {
  382. maptalks.Canvas.prepareCanvasFont(ctx, this._textSymbol)
  383. ctx.textBaseline = 'middle'
  384. const dx = this._textSymbol['textDx'] || 0
  385. const dy = this._textSymbol['textDy'] || 0
  386. const text = this._getClusterText(cluster)
  387. maptalks.Canvas.fillText(ctx, text, pt.sub(cluster['textSize'].x, 0)._add(dx, dy))
  388. }
  389. ctx.globalAlpha = opacity
  390. }
  391. _getClusterText(cluster) {
  392. const text = this.layer.options['textSumProperty'] ? cluster['textSumProperty'] : cluster['count']
  393. return text + ''
  394. }
  395. _getSprite() {
  396. if (!this._spriteCache) {
  397. this._spriteCache = {}
  398. }
  399. const key = maptalks.Util.getSymbolStamp(this._symbol)
  400. if (!this._spriteCache[key]) {
  401. this._spriteCache[key] = new maptalks.Marker([0, 0], { 'symbol': this._symbol })._getSprite(this.resources, this.getMap().CanvasClass)
  402. }
  403. return this._spriteCache[key]
  404. }
  405. _initGridSystem() {
  406. const points = []
  407. let extent, c
  408. this.layer.forEach(g => {
  409. if (!g.isVisible()) {
  410. return
  411. }
  412. c = g._getPrjCoordinates()
  413. if (!extent) {
  414. extent = g._getPrjExtent()
  415. } else {
  416. extent = extent._combine(g._getPrjExtent())
  417. }
  418. points.push({
  419. x: c.x,
  420. y: c.y,
  421. id: g._getInternalId(),
  422. geometry: g
  423. })
  424. })
  425. this._markerExtent = extent
  426. this._markerPoints = points
  427. }
  428. _computeGrid() {
  429. const map = this.getMap()
  430. const zoom = map.getZoom()
  431. if (!this._markerExtent) {
  432. this._initGridSystem()
  433. }
  434. if (!this._clusterCache) {
  435. this._clusterCache = {}
  436. }
  437. const pre = map._getResolution(map.getMinZoom()) > map._getResolution(map.getMaxZoom()) ? zoom - 1 : zoom + 1
  438. if (this._clusterCache[pre] && this._clusterCache[pre].length === this.layer.getCount()) {
  439. this._clusterCache[zoom] = this._clusterCache[pre]
  440. }
  441. if (!this._clusterCache[zoom]) {
  442. this._clusterCache[zoom] = this._computeZoomGrid(zoom)
  443. }
  444. map.fire('cluster-compute-grid')
  445. }
  446. _computeZoomGrid(zoom) {
  447. if (!this._markerExtent) {
  448. return null
  449. }
  450. const map = this.getMap()
  451. const r = map._getResolution(zoom) * this.layer.options['maxClusterRadius']
  452. const preT = map._getResolution(zoom - 1) ? map._getResolution(zoom - 1) * this.layer.options['maxClusterRadius'] : null
  453. let preCache = this._clusterCache[zoom - 1]
  454. if (!preCache && zoom - 1 >= map.getMinZoom()) {
  455. this._clusterCache[zoom - 1] = preCache = this._computeZoomGrid(zoom - 1)
  456. }
  457. // 1. format extent of markers to grids with raidus of r
  458. // 2. find point's grid in the grids
  459. // 3. sum up the point into the grid's collection
  460. const points = this._markerPoints
  461. const sumProperty = this.layer.options['textSumProperty']
  462. const grids = {}
  463. const min = this._markerExtent.getMin()
  464. let gx, gy, key, pgx, pgy, pkey
  465. for (let i = 0, len = points.length; i < len; i++) {
  466. const geo = points[i].geometry
  467. let sumProp = 0
  468. if (sumProperty && geo.getProperties() && geo.getProperties()[sumProperty]) {
  469. sumProp = geo.getProperties()[sumProperty]
  470. }
  471. gx = Math.floor((points[i].x - min.x) / r)
  472. gy = Math.floor((points[i].y - min.y) / r)
  473. key = gx + '_' + gy
  474. if (!grids[key]) {
  475. grids[key] = {
  476. 'sum': new maptalks.Coordinate(points[i].x, points[i].y),
  477. 'center': new maptalks.Coordinate(points[i].x, points[i].y),
  478. 'count': 1,
  479. 'textSumProperty': sumProp,
  480. 'children': [geo],
  481. 'key': key + ''
  482. }
  483. if (preT && preCache) {
  484. pgx = Math.floor((points[i].x - min.x) / preT)
  485. pgy = Math.floor((points[i].y - min.y) / preT)
  486. pkey = pgx + '_' + pgy
  487. grids[key]['parent'] = preCache['clusterMap'][pkey]
  488. }
  489. } else {
  490. grids[key]['sum']._add(new maptalks.Coordinate(points[i].x, points[i].y))
  491. grids[key]['count']++
  492. grids[key]['center'] = grids[key]['sum'].multi(1 / grids[key]['count'])
  493. grids[key]['children'].push(geo)
  494. grids[key]['textSumProperty'] += sumProp
  495. }
  496. }
  497. return this._mergeClusters(grids, r / 2)
  498. }
  499. _mergeClusters(grids, r) {
  500. const clusterMap = {}
  501. for (const p in grids) {
  502. clusterMap[p] = grids[p]
  503. }
  504. // merge adjacent clusters
  505. const merging = {}
  506. const visited = {}
  507. // find clusters need to merge
  508. let c1, c2
  509. for (const p in grids) {
  510. c1 = grids[p]
  511. if (visited[c1.key]) {
  512. continue
  513. }
  514. const gxgy = c1.key.split('_')
  515. const gx = +(gxgy[0])
  516. const gy = +(gxgy[1])
  517. //traverse adjacent grids
  518. for (let ii = -1; ii <= 1; ii++) {
  519. for (let iii = -1; iii <= 1; iii++) {
  520. if (ii === 0 && iii === 0) {
  521. continue
  522. }
  523. const key2 = (gx + ii) + '_' + (gy + iii)
  524. c2 = grids[key2]
  525. if (c2 && this._distanceTo(c1['center'], c2['center']) <= r) {
  526. if (!merging[c1.key]) {
  527. merging[c1.key] = []
  528. }
  529. merging[c1.key].push(c2)
  530. visited[c2.key] = 1
  531. }
  532. }
  533. }
  534. }
  535. //merge clusters
  536. for (const m in merging) {
  537. const grid = grids[m]
  538. if (!grid) {
  539. continue
  540. }
  541. const toMerge = merging[m]
  542. for (let i = 0; i < toMerge.length; i++) {
  543. if (grids[toMerge[i].key]) {
  544. grid['sum']._add(toMerge[i].sum)
  545. grid['count'] += toMerge[i].count
  546. grid['textSumProperty'] += toMerge[i].textSumProperty
  547. grid['children'] = grid['children'].concat(toMerge[i].children)
  548. clusterMap[toMerge[i].key] = grid
  549. delete grids[toMerge[i].key]
  550. }
  551. }
  552. grid['center'] = grid['sum'].multi(1 / grid['count'])
  553. }
  554. return {
  555. 'clusters': grids,
  556. 'clusterMap': clusterMap
  557. }
  558. }
  559. _distanceTo(c1, c2) {
  560. const x = c1.x - c2.x
  561. const y = c1.y - c2.y
  562. return Math.sqrt(x * x + y * y)
  563. }
  564. _stopAnim() {
  565. if (this._player && this._player.playState !== 'finished') {
  566. this._player.finish()
  567. }
  568. }
  569. onZoomStart(param) {
  570. this._stopAnim()
  571. super.onZoomStart(param)
  572. }
  573. onZoomEnd(param) {
  574. if (this.layer.isEmpty() || !this.layer.isVisible()) {
  575. super.onZoomEnd.apply(this, arguments)
  576. return
  577. }
  578. this._inout = param['from'] > param['to'] ? 'in' : 'out'
  579. this._animated = true
  580. this._computeGrid()
  581. super.onZoomEnd.apply(this, arguments)
  582. }
  583. _clearDataCache() {
  584. this._stopAnim()
  585. delete this._markerExtent
  586. delete this._markerPoints
  587. delete this._clusterCache
  588. delete this._zoomInClusters
  589. }
  590. })