做三维地图应用时,我们通常不只是需要一个地球。加载底图、管理设备点位、绘制区域、计算距离、展示建筑、添加天气效果,这些功能需要一起工作,还要处理页面切换后的事件和资源清理。
正式上线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、地图服务与第三方资源的授权和署名要求,应分别遵守。