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

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

前言

目前可视化大屏网站大致分为2/3D两个大类,据我所知3D大屏中的实现方式有客户端渲染和服务端渲染,客户端渲染主流的实现方式大概有threejsbabylonjs或者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>

技术实现

我们需要展示一条公交线路上不同站点乘客的流动情况,大白话就是乘客做几路公交车从哪里来,到哪里去,他们之间的关系用飞线在适合不过了,就像这样:

首先我们需要准备几个关键的数据文件:

  1. 站点数据 (stands.json) - 包含所有公交站点的位置和客流信息
  2. 飞线数据 (line.json) - 站点间的客流流向关系
  3. 线路数据 (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)

效果图

  1. 站点热力柱 - 用高度和渐变色展示客流强度
  2. 飞线效果 - 自定义shader实现流光动画,展示客流流向
  3. 线路路径 - 上下行用不同颜色区分
  4. 泛光效果 - 增强视觉

整个实现过程其实并不复杂,关键是要理解geo数据结构和简单的shader编程。下一期我们继续实现区域客流OD和实时公交功能。

代码仓库:zuo-wentao/bmap-demo: bmap demp

相关推荐
秋子aria6 小时前
模块的原理及使用
前端·javascript
菜市口的跳脚长颌6 小时前
一个 Vite 打包配置,引发的问题—— global: 'globalThis'
前端·vue.js·vite
胖虎2656 小时前
实现无缝滚动无滚动条的 Element UI 表格(附完整代码)
前端·vue.js
星链引擎6 小时前
企业级智能聊天机器人 核心实现与场景落地
前端
GalaxyPokemon6 小时前
PlayerFeedback 插件开发日志
java·服务器·前端
爱加班的猫6 小时前
深入理解防抖与节流
前端·javascript
自由日记7 小时前
学习中小牢骚1
前端·javascript·css
泽泽爱旅行7 小时前
业务场景-opener.focus() 不聚焦解决
前端
VOLUN7 小时前
Vue3 选择弹窗工厂函数:高效构建可复用数据选择组件
前端·javascript·vue.js