b-map-viewer:面向 Vue 3 的地图可视化 SDK,从接入到展示再到交互分析

做三维地图应用时,我们通常不只是需要一个地球。加载底图、管理设备点位、绘制区域、计算距离、展示建筑、添加天气效果,这些功能需要一起工作,还要处理页面切换后的事件和资源清理。

正式上线b-map-viewer 地图sdk,将这些常用能力组织成独立模块,让 Vue 3 项目可以围绕业务数据搭建地图gis场景,而不必在每个页面重复处理相同的初始化与图层管理代码。

本文介绍 b-map-viewer 的主要能力,并从一个可运行的 Vue 3 示例开始,逐步了解底图、图层、拾取绘制、空间分析和天气系统的使用方式,后续版本还会添加一些相关的粒子效果。

b-map-viewer 能够应用于设备监测、智慧园区管理、智慧城市可视化、三维 GIS、离线地图和专题分析等场景。

一、项目入口

项目名称为 BMapViewer ,npm 包名为 b-map-viewer。它是基于 Cesium 的 SDK,不是百度地图 JavaScript SDK;百度地图是其支持的底图来源之一。

二、BMapViewer 能做什么?

BMapViewer 保留了 Cesium 的原生扩展能力,同时将常用功能划分为以下模块:

模块 主要职责 典型用途
BMapViewer / useCesium Viewer 初始化、相机控制与生命周期管理 创建地图页面、控制视角
BaseMaps 多源影像、离线瓦片、地形与坐标适配 切换底图、加载真实地形
MapLayers 点、线、面、文字、热力图、3D Tiles 与动态效果 设备分布、运行状态、专题可视化
PickTools 点位拾取与图形绘制 地图选点、路线和范围输入
SpatialAnalysis 基于 Turf 的空间计算 距离、面积、缓冲区、范围查询
TerrainAnalysis 基于地形采样的三维分析 高程、剖面、通视、挖填方与淹没估算
WeatherEffects 雨、雪、雾、沙尘、云层与闪电 场景氛围和天气可视化

SDK 不接管业务接口、权限或应用状态,这些仍由接入项目管理。

1. 底图和业务图层分开管理

底图回答"场景的地理背景是什么",业务图层回答"要在地图上展示什么数据"。

例如,ArcGIS 影像通过 BaseMaps 加载,设备点通过 MapLayers 渲染。更新设备数据不需要重建底图,切换底图也不必重建所有业务图层。

2. 图层采用一致的数据组织方式

常用图层以 geometry 表达空间位置,以 properties 携带业务属性。数据处理、分析结果和图层展示之间可以复用这一结构。

3. 示例可以直接编辑运行

在线示例按底图、图层、拾取绘制、空间分析、三维分析和天气系统分类。展开左侧代码面板后,可以修改参数并运行当前示例。文档中的组件案例则用于查看对应效果。

三、安装与环境准备

本文以已有的 Vue 3 + Vite 项目为基础。

声明的 peer dependencies 为:

  • Vue:>=3.4.0 <4。
  • Cesium:>=1.118.0 <2。

下面固定使用项目验证过的 cesium@1.118.2,方便复现。声明的版本范围不代表其中所有版本都经过逐一测试。

使用 npm

bash 复制代码
npm install b-map-viewer cesium@1.118.2 vite-plugin-cesium

使用 pnpm

bash 复制代码
pnpm add b-map-viewer cesium@1.118.2 vite-plugin-cesium

两组命令选择一组执行即可。Vue、Vite 和 @vitejs/plugin-vue 由现有 Vue 3 工程提供。

配置 Vite

在 vite.config.ts 中配置 Cesium 静态资源处理:

ts 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import cesium from 'vite-plugin-cesium'

export default defineConfig({
  plugins: [vue(), cesium()],
})

vite-plugin-cesium 用于处理 Cesium 的 Workers、Assets、Widgets 等资源,不是 BMapViewer 的业务图层插件。

SDK 内部已经显式导入 Cesium,接入项目不需要再写 window.Cesium = Cesium。需要使用原生 Cesium API 时,正常执行 import * as Cesium from 'cesium' 即可。

四、一个完整的 Vue 3 接入示例

下面的组件包含 ArcGIS 影像底图、设备点聚合和文字标注。不需要额外准备图标文件:聚合图层使用内置点位图标。

将它保存为 MapDemo.vue,再在页面中引入。

vue 复制代码
<script setup>
import { onBeforeUnmount, ref } from 'vue'
import { BMapViewer, BaseMaps, MapLayers } from 'b-map-viewer'
import 'b-map-viewer/style.css'

const mapRef = ref(null)
const resources = []

const camera = {
  longitude: 125.834,
  latitude: 44.147,
  height: 12000,
  pitch: -45,
  minHeight: 40,
  maxHeight: 800000,
}

const devices = [
  {
    geometry: { type: 'Point', coordinates: [125.827, 44.145] },
    properties: { id: 'device-1', name: '一号监测站' },
  },
  {
    geometry: { type: 'Point', coordinates: [125.834, 44.147] },
    properties: { id: 'device-2', name: '二号监测站' },
  },
  {
    geometry: { type: 'Point', coordinates: [125.842, 44.151] },
    properties: { id: 'device-3', name: '三号监测站' },
  },
]

function handleReady(viewer) {
  const baseMap = new BaseMaps.BaseMap(viewer, {
    type: 'arcgis',
    url: 'https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer',
    maximumLevel: 23,
  })
  resources.push(baseMap)
  baseMap.readyPromise.catch((error) => {
    console.error('底图加载失败:', error)
  })

  const markers = new MapLayers.IconClusterBasicLayer(viewer, {
    width: 30,
    height: 38,
    pixelRange: 48,
    minimumClusterSize: 2,
    clusterStyles: [
      { min: 2, size: 36, color: '#1c86d1dd' },
      { min: 20, size: 44, color: '#43b86add' },
    ],
  })
  resources.push(markers)
  markers.setData(devices)

  const labels = new MapLayers.TextGroupLayer(viewer, {
    fontSize: 15,
    color: '#ffffff',
    offset: [0, -48],
    showBackground: true,
    backgroundColor: 'rgba(7, 27, 40, 0.85)',
    border: false,
    maxWidth: 160,
    overflowMode: 'ellipsis',
    enableCollisionDetection: true,
  })
  resources.push(labels)
  labels.setData(devices.map((item) => ({
    geometry: item.geometry,
    properties: {
      id: 'label-' + item.properties.id,
      text: item.properties.name,
    },
  })))
}

function handleError(error) {
  console.error('地图初始化失败:', error)
}

onBeforeUnmount(() => {
  // 业务资源由创建它们的页面释放;Viewer 由 BMapViewer 组件管理。
  resources.reverse().forEach((resource) => resource.destroy())
  resources.length = 0
})
</script>

<template>
  <div class="map-demo">
    <BMapViewer
      ref="mapRef"
      :camera="camera"
      :scene-mode="1"
      @ready="handleReady"
      @error="handleError"
    />
  </div>
</template>

<style scoped>
.map-demo {
  width: 100%;
  height: 600px;
}
</style>

ArcGIS 影像需要网络访问。接入生产环境时,应确认所用地图服务的访问条件、授权要求和署名规则。

设备聚合与文字碰撞检测是两项独立能力:上例中的文字图层不会跟随设备点自动合并。需要只显示聚合统计时,可以不创建独立文字图层。

初始化时需要注意什么?

  • camera 有有效值时,组件挂载后自动初始化。
  • 不传 camera 时,需要通过组件 ref 调用 initMap(camera)。
  • ready 返回原生 Cesium.Viewer,每个组件挂载周期只触发一次。
  • 本 SDK 的 sceneMode 参数约定为 0 表示 2D,其他值表示 3D;它不是直接透传 Cesium 的枚举值。
  • 地图父容器必须具有明确高度。

需要等待接口返回相机参数时,可以采用手动初始化:

js 复制代码
// 在组件已挂载、mapRef 可用之后调用;模板中不要再传 camera。
const viewer = await mapRef.value.initMap({
  longitude: 125.834,
  latitude: 44.147,
  height: 12000,
  pitch: -45,
})

if (!viewer) {
  console.error('初始化未完成,请检查 error 事件')
}

重复调用 initMap 会复用已存在的 Viewer。显式重建使用 reinitializeMap,重建前应先清理旧 Viewer 上的业务资源;重建不会再次触发 ready,需要使用方法返回的新 Viewer。

五、用 BaseMaps 管理底图与地形

以下片段中的 viewer 均指 ready 回调返回的实例。它们是独立用法示例,不需要全部同时加入一个场景。

1. 加载离线瓦片

将自己的瓦片放入项目 public/tiles,按数据的实际层级和坐标系配置:

js 复制代码
import { BaseMaps } from 'b-map-viewer'

const tileUrl = `${import.meta.env.BASE_URL}tiles/{z}/{x}/{reverseY}.png`

const baseMap = new BaseMaps.BaseMap(viewer, {
  type: 'offline',
  url: tileUrl,
  crs: 'WGS84',
  minimumLevel: 1,
  maximumLevel: 12,
})

这里的 URL 采用 TMS 反向行号 {reverseY}。如果数据使用 XYZ 行号,应换为 {y}。npm 包不会附带你项目的离线瓦片和 3D Tiles 数据,需要自行部署。

2. 切换在线底图

js 复制代码
baseMap.switch({
  type: 'amap',
  style: 'elec',
  crs: 'GCJ02',
  maximumLevel: 18,
})

await baseMap.readyPromise

除高德外,底图模块还支持百度、腾讯、天地图、星图、Google、ArcGIS 和 Cesium 官方影像。各服务的 style、Key、Token 和可访问性不同,应按对应 Provider 文档配置。

当sdk应用于商业项目时,使用商业地图请购买对应图商的地图服务,添加对应图商的key调用对应地图服务,例如使用百度地图时传入百度地图key、高德传入高德对应的地图key。

crs 应按数据来源与 Provider 规则选择,不能只根据地图品牌猜测。以高德 Provider 为例:业务坐标已经是 GCJ02 时使用 GCJ02,无需再次纠偏;业务坐标是 WGS84 时使用 WGS84,由 Provider 做相应适配。

3. 加载 Cesium World Terrain

影像和地形是两个独立资源:影像提供地表纹理,地形提供高程起伏。

js 复制代码
import { BaseMaps } from 'b-map-viewer'

const terrain = new BaseMaps.CesiumTerrain(viewer, {
  token: import.meta.env.VITE_CESIUM_ION_TOKEN,
  requestVertexNormals: true,
  requestWaterMask: true,
  depthTestAgainstTerrain: true,
})

await terrain.readyPromise

// 不再使用时,恢复之前的地形。
// terrain.destroy()

在本地 .env.local 配置自己申请的凭证:

dotenv 复制代码
VITE_CESIUM_ION_TOKEN=替换为自己的CesiumIonToken

前端环境变量会进入浏览器代码,并不是秘密存储。应使用适合前端公开使用的最小权限 Token,并配置服务允许的访问限制。

readyPromise 表示 Provider 已就绪,不代表当前视野的所有瓦片已渲染完成。CesiumTerrain 本身不会移动相机,视角由业务页面单独控制。

六、图层的数据格式与选择

常用点、线、面图层使用以下结构:

js 复制代码
const pointData = [{
  geometry: {
    type: 'Point',
    coordinates: [125.834, 44.147, 0],
  },
  properties: {
    id: 'device-001',
    name: '监测设备',
    status: 'online',
  },
}]

const lineData = [{
  geometry: {
    type: 'LineString',
    coordinates: [[125.82, 44.14], [125.84, 44.15]],
  },
  properties: { id: 'route-001' },
}]

const polygonData = [{
  geometry: {
    type: 'Polygon',
    coordinates: [[
      [125.82, 44.14],
      [125.84, 44.14],
      [125.84, 44.16],
      [125.82, 44.16],
      [125.82, 44.14],
    ]],
  },
  properties: { id: 'area-001' },
}]

坐标顺序是 [经度, 纬度, 可选高度],不是 [纬度, 经度]。经纬度单位为度,高度单位为米;具体高度参考与贴地行为取决于图层配置。

这是一套基于 GeoJSON 几何结构的 SDK 数据约定,不意味着每个图层都支持所有 GeoJSON 类型或 FeatureCollection。例如图标聚合只接受点数据,3D Tiles 图层则有自己的资源加载接口。

常用图层

  • 标注:IconGroupLayer、TextGroupLayer、LabelGroupLayer、BubbleLayer、BubbleGroupLayer。
  • 线面:LineGroupLayer、PolygonPrimitiveLayer、MaskLayer、CircleGroupLayer。
  • 专题数据:HeatmapLayer、HeatmapPrimitiveLayer、Heatmap3DLayer。
  • 三维场景:Build3DLayer、GradientWallLayer、RadarScanner3DLayer。
  • 动态效果:FlowLineLayer、LineLightingTrailLayer、WallTrailLayer、DiffuseWallLayer、RadarScanLayer、DynamicWaterLayer、WaterFlowLayer、FloodLayer。

不必一开始记住全部参数。可以先在图层示例里找到目标效果,再查对应图层文档。

七、拾取 → 分析 → 图层展示

地图分析可以拆成三个职责:PickTools 收集用户输入,分析类返回计算结果,MapLayers 负责可视化。这样既可以通过鼠标输入,也可以用接口返回的坐标调用同一套分析方法。

1. 绘制一条路径并显示分段距离

下面将完整流程封装成一个函数。调用时传入 Viewer,离开页面时执行返回的清理方法。

js 复制代码
import { PickTools, SpatialAnalysis, MapLayers } from 'b-map-viewer'

function startDistanceMeasurement(viewer) {
  const analysis = new SpatialAnalysis({ unit: 'km' })
  const route = new MapLayers.LineGroupLayer(viewer, {
    color: '#45eadf',
    width: 4,
  })
  const labels = new MapLayers.TextGroupLayer(viewer, {
    fontSize: 15,
    color: '#ffffff',
    showBackground: true,
    backgroundColor: 'rgba(7, 27, 40, 0.9)',
    offset: [0, -20],
    enableCollisionDetection: false,
  })
  const tools = new PickTools(viewer, {
    color: '#45eadf',
    lineWidth: 4,
    isReserve: false,
    mouseHints: { show: true, text: '左键添加折点,双击结束' },
  })

  tools.pickLine((coordinates) => {
    const result = analysis.getDistance(coordinates)

    route.setData([{
      geometry: { type: 'LineString', coordinates },
      properties: { id: 'measured-route' },
    }])

    labels.setData([
      ...result.segments.map((segment) => ({
        geometry: { type: 'Point', coordinates: segment.center },
        properties: {
          id: 'segment-' + segment.index,
          text: segment.distance.toFixed(3) + ' km',
        },
      })),
      {
        geometry: {
          type: 'Point',
          coordinates: result.segments.at(-1).end,
        },
        properties: {
          id: 'total',
          text: '总距离:' + result.totalDistance.toFixed(3) + ' km',
        },
      },
    ])

    console.log('距离分析结果:', result)
  })

  return () => {
    tools.destroy()
    labels.destroy()
    route.destroy()
  }
}

// 在 ready 回调中启动:
// const stopMeasurement = startDistanceMeasurement(viewer)
// 页面退出或工具切换时调用 stopMeasurement()

getDistance 按输入顺序计算相邻点之间的球面距离,返回 distances、segments、totalDistance 和 unit。分段详情包含 center、distance 与 cumulativeDistance,可以直接用于标注。

它不会计算高度差或地形起伏。如果需要沿起伏地表计算距离,应使用 TerrainAnalysis.getSurfaceDistance。

2. 绘制能力不止点和线

PickTools 还提供图标点、多边形、正方形、矩形、圆形和椭圆等绘制方法。圆形等形状的尺寸输出可以通过 unit: 'm' 或 'km' 配置;需要返回带高度的拾取坐标时,可以启用 includeHeight: true。

例如,多边形坐标可用于 getArea,两组面坐标可用于 getOverlapArea,圆心和半径可用于 getCircleArea。请同时核对分析方法的输入单位与输出单位配置。

空间分析还涵盖缓冲区、范围内点位查询、周边查询、最近点、区域合并与差集、线面穿越、覆盖率等能力,具体输入与返回结构见空间分析文档。

八、基于真实地形进行三维分析

TerrainAnalysis 与普通空间分析不同,它需要真实地形数据,并通过异步采样计算结果。

在已经加载好地形的 Viewer 上,可以查询一条路线的高程剖面:

js 复制代码
import { TerrainAnalysis } from 'b-map-viewer'

const analysis = new TerrainAnalysis(viewer, {
  unit: 'km',
  areaUnit: 'km2',
  sampleSpacing: 60,
  maxSamples: 4096,
})

try {
  const result = await analysis.getProfile([
    [117.914, 43.783],
    [117.945, 43.791],
  ])
  console.log('高程剖面:', result)
} catch (error) {
  console.error('地形采样失败:', error)
} finally {
  analysis.destroy()
}

示例中的坐标也可以换成 PickTools.pickLine 返回的坐标。分析类只返回数据,不自动创建结果图层;路线、标签和填色区域仍由业务代码交给 MapLayers 展示。

除了剖面,还可以进行地形高程、贴地距离、地表面积、坡度坡向、地形通视、挖填方和淹没估算。动态淹没可结合 prepareFlood 与 FloodLayer,复用采样结果并展示水位变化。

需要注意:

  • 精度取决于地形数据与采样密度,不是无限精确的连续地表计算。
  • 高程使用米,表示 WGS84 椭球高,不等同于离地高度或通常所说的海拔。
  • 通视分析只考虑地形,不包含建筑和 3D Tiles 遮挡。
  • 淹没估算不包含水动力、流速、堤坝及连通性模拟,不能直接当作专业防洪预测。
  • 页面切换时应销毁分析实例,或通过 AbortSignal 让旧任务失效,避免迟到结果更新新场景。

九、添加天气系统

天气系统通过独立实例进行管理,可以按需要组合和动态调整参数。

js 复制代码
import { WeatherEffects } from 'b-map-viewer'

const rain = new WeatherEffects.RainEffect(viewer, {
  intensity: 0.4,
  density: 0.7,
  speed: 1,
  size: 0.7,
  angle: -20,
  lightning: false,
})

// 动态调整,不必重新创建实例。
rain.setOptions({ speed: 1.3, density: 0.9 })

// 按交互需要调用:
// rain.hide()
// rain.show()
// rain.destroy()

降雨、降雪可以配置方向、速度、密度和大小;雾用于表现距离或高度相关的遮挡;云层用于构建更丰富的天空效果。CloudEffect 和 CloudUltraEffect 提供不同的体积云实现,建议根据设备性能选择。

雨雪等屏幕空间效果与体积云的渲染方式不同,不能将它们都理解为世界空间中的实体。调整相机到能看到天空的低空斜视角,通常更便于观察天气效果。

十、接入时最容易忽略的几个问题

地图没有显示

先检查容器高度,再检查 camera 是否传入、ready 或 error 事件是否触发,以及浏览器中的 Cesium 静态资源请求是否成功。

底图加载成功,但业务点位偏移

确认数据是 WGS84、GCJ02 还是 BD09,按对应 Provider 文档配置 crs。影像、业务坐标和分析输入不应混用不同坐标体系。

页面切换后,旧结果覆盖新结果

资源创建后就应登记清理方法,不要等全部异步请求完成后才记录。每次任务保留独立的资源归属;切换时销毁旧实例,让旧请求返回后不再更新当前页面。

也不要用 viewer.entities.removeAll() 或全局影像差集来清理单个业务功能,否则可能影响其他图层。

ready 之后还没有完整影像或地形

组件 ready 表示 Viewer 可以使用;底图或地形的 readyPromise 表示 Provider 准备完成。当前视野的瓦片仍可能继续加载,这是不同阶段。

离线瓦片报 404

检查部署路径、文件扩展名、最小与最大层级,以及行号约定究竟是 {y} 还是 {reverseY}。使用 import.meta.env.BASE_URL 可以避免子路径部署时把静态资源误请求到域名根目录。

动态效果越来越卡

检查是否重复创建了天气阶段、动态图层、鼠标事件或相机监听。对暂时不使用的效果调用隐藏方法,对不再使用的实例调用 destroy()。不要把隐藏等同于资源已经释放。

十一、总结

b-map-viewer 的重点,是让地图应用围绕清晰的职责协作:组件管理 Viewer,底图模块管理地理背景,图层展示业务数据,拾取工具负责输入,分析类负责计算,天气系统补充场景效果。

对于 Vue 3 开发者,可以先完成一个底图和设备点位页面,再逐步加入交互、分析和三维效果;需要更细的控制时,仍然可以通过 ready 返回的 Viewer 使用原生 Cesium API。

欢迎通过在线示例体验效果,在技术文档查阅参数。

项目采用 Apache-2.0 许可证。Cesium、地图服务与第三方资源的授权和署名要求,应分别遵守。

相关推荐
10年前端老司机1 小时前
Agent 商业化困局:热闹的发布会,冷清的付费客户
前端·后端·agent
钱栈up2 小时前
热榜上手|HyperFrames:写HTML出MP4
前端·html
Sailing2 小时前
🚀🚀【封闭结束8】Agent RAG:从零搭建一个可用知识库
前端·后端·agent
Captain1314_李祖团2 小时前
5. MLIR相关语法
java·前端·javascript
齐 飞2 小时前
VUE2常用语法总结
javascript·vue.js
恋猫de小郭2 小时前
Flutter 可以更直接地调用原生 API 了,Dart Direct Native Interop
android·前端·flutter
Data analyse4562 小时前
web-vitals库接入:LCP、INP、CLS上报与阈值
前端·javascript·数据分析
北城笑笑2 小时前
Vue 106 ,上篇:前端桌面端应用实现方案( Electron 框架详细介绍,Vue + Electron )
前端·javascript·vue.js·python·electron