Mapbox GL JS CookbookMapbox GL JS Cookbook
快速开始
样式规范
空间数据
插件
进阶
DECK.GL
  • 文档
  • 术语
  • 示例
GitHub
快速开始
样式规范
空间数据
插件
进阶
DECK.GL
  • 文档
  • 术语
  • 示例
GitHub
<template>
  <base-map :map-options="mapOptions" @load="handleMapLoaded" />
</template>

<script setup lang="ts">
import BaseMap from '../base-map.vue'
import mapboxgl from 'mapbox-gl'
import turfDistance from '@turf/distance'
import { withBase } from 'vuepress/client'
import type { Position } from 'geojson'
import { STYLE } from '../../../utils/constant'
import * as echarts from 'echarts/core'
import { PieChart } from 'echarts/charts'
import { TitleComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'

echarts.use([PieChart, TitleComponent, CanvasRenderer])

interface DataProperties {
  id: string | number
  name: string
  v1: number
  v2: number
  v3: number
  coords?: Position
}

interface ClusterProperties {
  cluster: boolean
  cluster_id: number
  point_count: number
  point_count_abbreviated: number
}

const mapOptions: Omit<mapboxgl.MapboxOptions, 'container'> = {
  style: STYLE.GRAY,
  center: [107.744809, 30.180706],
  zoom: 6,
  minZoom: 4,
}

const handleMapLoaded = (map: mapboxgl.Map) => {
  const SOURCE_ID = 'point-source'
  const PIE_WIDTH = 80
  // 弹窗
  const popup = new mapboxgl.Popup({
    closeButton: false,
    closeOnClick: false,
    offset: [0, -(PIE_WIDTH / 2)],
  })
  map.addSource(SOURCE_ID, {
    type: 'geojson',
    data: withBase('/data/point.geojson'),
    cluster: true,
    clusterRadius: 85, // 聚合半径,比饼图 DOM 略大即可
  })
  // 必须添加图层,后面才能取到要素
  map.addLayer({
    id: 'earthquake_circle',
    type: 'circle',
    source: SOURCE_ID,
    paint: {
      'circle-radius': 0, // 设置半径为 0,不显示
    },
  })

  // 数据加载或更改时,更新饼图
  map.on('data', (e) => {
    if (e.sourceId !== SOURCE_ID || !e.isSourceLoaded) return
    map.on('move', updateMarkers)
    map.on('moveend', updateMarkers)
    updateMarkers()
  })

  // 缓存所有 markers,较少重复添加,提高性能
  const markers = {}
  // 地图上显示的 markers
  let markersOnScreen = {}

  async function updateMarkers() {
    // 更新后的 markers
    const newMarkers = {}
    const features = map.querySourceFeatures(SOURCE_ID)
    const geojsonSource = map.getSource(SOURCE_ID) as mapboxgl.GeoJSONSource

    // 给所有要素添加 marker
    for (let i = 0; i < features.length; i++) {
      const feature = features[i] as GeoJSON.Feature<GeoJSON.Point>
      const coords = feature.geometry.coordinates

      let id: string | number
      let props: DataProperties

      if (feature.properties && feature.properties.cluster) {
        // 聚合要素
        id = (feature.properties as ClusterProperties).cluster_id
        // 获取聚合要素中离该点最近的点的属性
        props = await getClusterLeaves(id, geojsonSource, feature)
      } else {
        // 非聚合要素
        props = feature.properties as DataProperties
        id = props.id
      }

      let marker = markers[id]
      // 如果该要素没有对应的 marker,则新建 marker
      if (!marker) {
        props.coords = coords
        marker = markers[id] = new mapboxgl.Marker({
          element: createPieChart(props),
        }).setLngLat(coords as mapboxgl.LngLatLike)
      }
      newMarkers[id] = marker
      // 如果未添加到地图,则添加到地图
      if (!markersOnScreen[id]) {
        marker.addTo(map)
      }
    }
    // 移除不可见的 marker
    for (const id in markersOnScreen) {
      if (!newMarkers[id]) {
        markersOnScreen[id].remove()
      }
    }
    markersOnScreen = newMarkers
  }

  /**
   * 创建 echart pie
   * @param props 点属性
   */
  function createPieChart(props: DataProperties): HTMLElement {
    const el = document.createElement('div')
    el.style.width = `${PIE_WIDTH}px`
    el.style.height = `${PIE_WIDTH}px`
    const pieChart = echarts.init(el)
    const { name, coords } = props
    pieChart.setOption({
      title: {
        show: true,
        text: name.length > 4 ? `${name.substring(0, 3)}··` : name,
        left: 'center',
        top: 'center',
        textStyle: {
          fontSize: 12,
          textShadowBlur: 5,
          textShadowColor: '#3EAF7C',
        },
        padding: 0,
        shadowColor: 'rgba(0, 0, 0, 0.5)',
        shadowBlur: 10,
      },
      color: ['#FFD273', '#E86D68', '#A880FF'],
      series: [
        {
          type: 'pie',
          name: `${name}|${coords}`, // 将坐标放在系列名称,以便鼠标事件可以正确显示弹窗
          radius: ['40%', '96%'],
          center: ['50%', '50%'],
          hoverAnimation: false,
          label: {
            show: true,
            position: 'inside',
            formatter: '{c}',
          },
          labelLine: {
            show: false,
          },
          data: [
            { value: props.v1, name: '土豆' },
            { value: props.v2, name: '玉米' },
            { value: props.v3, name: '红薯' },
          ],
        },
      ],
    })
    // 鼠标经过饼图显示弹窗
    pieChart.on('mouseover', (params) => {
      const { seriesName, data } = params
      const nameAndCoords = seriesName!.split('|')
      const name = nameAndCoords[0]
      const coords = nameAndCoords[1].split(',').map((item) => Number(item)) as mapboxgl.LngLatLike
      // @ts-ignore
      const description = `<p style="padding: 0 10px;">${name}: ${data.name} ${data.value} 万顷</p>`
      popup.setLngLat(coords).setHTML(description).addTo(map)
    })
    pieChart.on('mouseout', () => {
      popup.remove()
    })
    return el
  }

  /**
   * 获取离聚合点最近的要素原始属性
   * @param clusterId 聚合 id
   * @param geojsonSource GeoJSON 数据源
   * @param targetPoint 聚合点要素
   */
  function getClusterLeaves(
    clusterId: number,
    geojsonSource: mapboxgl.GeoJSONSource,
    targetPoint: GeoJSON.Feature<GeoJSON.Point>
  ): Promise<DataProperties> {
    return new Promise((resolve, reject) => {
      geojsonSource.getClusterLeaves(clusterId, 30, 0, (error, features) => {
        if (error) {
          reject(error)
        }
        // 因为聚合点与实际点有一定的偏差,这里取离聚合点最近的要素点
        let minDist = Infinity
        let nearestFeatureIndex = 0
        for (let i = 0; i < features.length; i++) {
          const distance = turfDistance(targetPoint, features[i] as GeoJSON.Feature<GeoJSON.Point>)
          if (distance < minDist) {
            nearestFeatureIndex = i
            minDist = distance
          }
        }
        const props = features[nearestFeatureIndex].properties as DataProperties
        resolve(props)
      })
    })
  }
}
</script>
在 GitHub 上编辑此页!
上次更新:
贡献者: huangli, 黄俐