基于百度地图JSAPI Three的城市公交客流可视化(一)------线路客流

前言
目前可视化大屏网站大致分为2/3D两个大类,据我所知3D大屏中的实现方式有客户端渲染和服务端渲染,客户端渲染主流的实现方式大概有threejs、babylonjs或者unity打包html的方式,而服务端则由UE像素流技术实现,当然他们也各有各的优缺点。
今天我们的需求是快速的实现在公交客流的炫酷的可视化展示(ps:一定要高级),我们需要在地图上展示最基础的GEO数据,像点、线等业务数据,目前市面上的有很多可用:
-
cesium:相对复杂 -
高德地图:loca可视化拓展性低,叠加gl图层复杂 -
mapbox:(不会,嘻嘻)
调研了很多,百度地图居然出了基于threejs的版本,大致看了一下,不仅封装了很多模块例如:天气系统、动态天空(还包含大气散射、体积云)、众多投影方式和3D地球,这简直就是百度版cesium(bushi ,不仅能实现地图的基础业务还能拓展超级多的炫酷效果,废话不多说,让我们一块开始实现我们的业务。
需要实现的公交客流效果:线路客流 OD、区域客流 OD和实时公交 ,如图所示

技术准备和项目初始化
我们需要去百度地图开放平台申请AK(不是你想的那个),🔗:控制台 | 百度地图开放平台 这里跟着官网流程走就行
首先我们使用的技术栈是Vue3,安装我们需要使用的依赖:
-
@baidumap/mapv-three百度JSAPI three -
@turf/turf空间计算工具 -
threejs -
vite-plugin-static-copy(vite原生插件替代rollup-plugin-copy)
安装vite-plugin-static-copy后将以下主要代码写进vite.config.ts
javascript
// vite-plugin-static-copy
import { viteStaticCopy } from 'vite-plugin-static-copy'
export default defineConfig({
plugins: [
viteStaticCopy({
targets: [
{
src: 'node_modules/@baidumap/mapv-three/dist/assets/*',
dest: 'mapvthree/assets',
},
],
}),
// 其他插件...
],
// ...其他配置
})
接着我们就去项目的index.html中加入MAPV_BASE_URL 全局变量指向静态资源目录,前提是一定要安装了mapv-three
html
<script>
window.MAPV_BASE_URL = '/node_modules/@baidumap/mapv-three/dist' // 配置为mapv-three包路径的dist目录
</script>
配置完成后初始化地图我们需要装填我们的AK,然后再开始真正的开发流程。
javascript
<script setup lang="ts">
import { onMounted } from 'vue'
// 在项目入口文件中配置
import * as mapvthree from '@baidumap/mapv-three'
import type { Engine } from '@baidumap/mapv-three/dist/types/index.d.ts'
// 配置百度地图 AK
mapvthree.BaiduMapConfig.ak = 'xxxxxxxxxxxx' //你自己申请的AK
onMounted(() => {
const div = document.getElementById('engine') as HTMLDivElement | null
if (!div) return
const engine: Engine = new mapvthree.Engine(div, {
map: {
center: [116.404, 39.915],
range: 7000,
projection: 'EPSG:3857',
},
rendering: {
enableAnimationLoop: true,
sky: new mapvthree.DynamicSky({
time: 3600 * 6,
}), // 动态天空
},
} as any)
})
</script>
<template>
<div id="engine" class="engine"></div>
</template>
<style scoped>
.engine {
width: 100%;
height: 100%;
}
</style>
技术实现
我们需要展示一条公交线路上不同站点乘客的流动情况,大白话就是乘客做几路公交车从哪里来,到哪里去,他们之间的关系用飞线在适合不过了,就像这样:

首先我们需要准备几个关键的数据文件:
- 站点数据 (
stands.json) - 包含所有公交站点的位置和客流信息 - 飞线数据 (
line.json) - 站点间的客流流向关系 - 线路数据 (
route.json) - 公交线路的上下行路径
站点数据结构:
json
{
"stationId": 200,
"stationName": "火车站",
"stands": [
{
"id": 799,
"name": "火车站",
"lat": 33.3195783,
"lon": 117.30184,
"down": 219 // 下车人数
}
]
}
飞线数据定义了客流流向:
json
{
"from": [{ "id": 765, "down": 2 }], // 从id:765站点出发
"to": [{ "id": 561, "down": 2 }] // 到达id:561站点
}
站点可视化 - 热力柱
站点我们用热力柱来展示,高度代表客流强度。这里用到了mapv-three的Pillar热力柱:
ts
// 展平所有站点数据
const allStands = stands.flatMap((s) => s.stands)
const features = allStands.map((s) => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [wgs84tobd09(s.lon, s.lat)[0], wgs84tobd09(s.lon, s.lat)[1]],
},
properties: {
id: s.id,
name: s.name,
down: s.down || 0, // 下车人数作为高度依据
},
}))
// 创建热力柱
const pillar = new mapvthree.Pillar({
shape: 'pillar',
height: 500, // 基础高度
radius: 10, // 柱子半径
vertexHeights: true, // 启用顶点高度
gradient: {
0.02: '#467ce3', // 蓝色
0.04: '#25ba3d', // 绿色
0.06: '#b6cc4f', // 黄绿色
0.5: '#b04932', // 红色
},
})
// 设置高度映射
dataSource.defineAttribute('height', (p) => p.down * 2)
wgs84tobd09函数 是将硬件设备上常用的定位格式转为百度地图专用的格式
我们用了从蓝色到红色渐变色来区分不同客流强度,如图所示:

飞线效果实现
飞线是客流可视化的核心,但是百度地图文档中似乎并没有封装高自定义得飞线,但是好处是百度地图允许我们使用一切threejs中的功能,准备基于Three.js的Line功能,手写自定义的FlyLine类来实现这个类:
ts
export class FlyLine extends Line {
constructor(startBd09: LngLat, endBd09: LngLat, options?: FlyLineOptions) {
// 坐标转换
const start = lngLatToMercator(startBd09)
const end = lngLatToMercator(endBd09)
// 动态计算弧线高度
const distance = start.distanceTo(end)
const height = this.calculateHeight(distance, options)
// 构建贝塞尔曲线
const points = buildSymmetricArc(start, end, height, segments,offset)
// 创建流光shader材质
const shaderMaterial = new ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uSpeed: { value: speed },
uBaseColor: { value: baseColor },
uGlowColor: { value: glowColor },
},
vertexShader,
fragmentShader,
transparent: true,
blending: AdditiveBlending, // 叠加混合产生发光效果
})
super(geometry, shaderMaterial)
}
}
其中飞线主要核心函数是buildSymmetricArc ,主要原理是在两个平面坐标点之间,构建一条光滑的三维贝塞尔弧线,其顶点在连接线的上,从而形成一个拱形的多线段曲线,其中segments为线段平滑程度,offset为贝塞尔曲线偏移量 如图所示:
关键点在于让曲线动起来,这里采用流光shader的实现,我们在fragment来模拟光点沿着线条流动的效果,如果你是Vscode,推荐安装插件:WebGL GLSL Editor,这样可以抽离成glsl文件并且编写时有语法提示,这里我们暂用模板语法
c
// 流光shader着色器
const fragmentShader = `
uniform float uTime;
uniform float uSpeed;
uniform vec3 uBaseColor;
uniform vec3 uGlowColor;
varying float vU;
void main() {
// 计算流光位置
float flowPos = fract(vU - uTime * uSpeed);
// 主流光 - 明亮的光点
float mainFlow = 1.0 - smoothstep(0.0, 0.1, abs(flowPos - 0.5));
// 流光尾巴
float trail = smoothstep(flowPos - uTrail, flowPos, vU);
// 颜色混合,让流光部分超出RGB范围产生泛光
vec3 color = mix(uBaseColor, uGlowColor, mainFlow + trail);
color = mix(color, color * 3.0, mainFlow + trail);
gl_FragColor = vec4(color, intensity * uOpacity);
}
`;
飞线泛光效果
好了,为了让效果更炫酷,我们需要泛光效果,如果是在threejs中我们需要手动的增加后处理加入Bloom效果,分层、合成会较为繁琐,百度地图为我们考虑好了这点,我们直接在engine.rendering.features.bloom中即可开启场景泛光,只要我们的颜色超过RGB的范围,就会产生泛光
ts
const engine = new mapvthree.Engine(div, {
rendering: {
features: {
bloom: {
enabled: true,
strength: 10.5, // 泛光强度
threshold: 0.1, // 泛光阈值
radius: 1.4, // 泛光半径
},
},
},
})
//物体泛红光:
const box = engine.add(new Mesh(new IcosahedronGeometry(5, 15), new MeshBasicMaterial({
color: new Color(10, 0, 0), // 颜色超出RGB范围,会产生泛光效果
})));
这样飞线的流光效果就能产生真正的泛光了,看起来特别酷。

线路路径绘制
最后我们还需要绘制公交线路的上下行路径,这里直接使用自带的宽线就行,用不同颜色区分:
js
// 上行线路 - 浅蓝色
const upLine = engine.add(
new mapvthree.Polyline({
flat: true,
lineWidth: 4,
color: '#87CEFA',
}),
)
// 下行线路 - 红色
const downLine = engine.add(
new mapvthree.Polyline({
flat: true,
lineWidth: 4,
color: '#FF6B6B',
}),
)
// 设置数据源
upLine.dataSource = mapvthree.GeoJSONDataSource.fromGeoJSON(upLineGeoJSON)
downLine.dataSource = mapvthree.GeoJSONDataSource.fromGeoJSON(downLineGeoJSON)
效果图

- 站点热力柱 - 用高度和渐变色展示客流强度
- 飞线效果 - 自定义shader实现流光动画,展示客流流向
- 线路路径 - 上下行用不同颜色区分
- 泛光效果 - 增强视觉
整个实现过程其实并不复杂,关键是要理解geo数据结构和简单的shader编程。下一期我们继续实现区域客流OD和实时公交功能。