基于websocket使用mapbox结合threebox实现车辆实时轨迹展示
发表于:2023-03-20 | 分类: 前端
字数统计: 1.5k | 阅读时长: 7分钟 | 阅读量:
最近项目中有遇到根据实时推送数据展示轨迹的需求,并且需要加载gltfglb此类3D模型作为车辆点,所以在此需要结合threejs
但是若使用原生threejs的话结合地图开发不太友好,所以使用threebox作为桥梁可以减少很多心智负担。在这里做个小demo记录一下

  1. 引入所需的 js 库
    1
    2
    3
    4
    5
    6
    7
    8
     // mapboxgl
    <script type="text/javascript" src="xxx/mapbox-gl.js"></script>
    <link rel="stylesheet" type="text/css" href="xxx/mapbox-gl.css"/>
    // threebox
    <script type="text/javascript" src="xxx/threebox/threebox.js"></script>
    <link rel="stylesheet" type="text/css" href="xxx/threebox/threebox.css" />
    // turf
    <script type="text/javascript" src="./turf/turf.min.js"></script>
  2. 创建一个地图容器
    1
    <div class="map-container" id="map"></div>
  3. 声明一些全局变量
    1
    2
    3
    4
    5
    let vehicleModel = null // 模型实例
    let socket = null // websocket实例
    let idLayer = null // 车牌号图层
    let map = null // 地图实例
    let wsURL = 'ws://x.x.x.x:xxxx/xxx' // websocket url
  4. 根据推送数据渲染/更新模型属性

    注意:当前websocket接口返回每一帧的数据格式为
    [{ "dateTime": "2023-02-10 10:08:20:352", "originalType": 10, "latitude": 31.4247565, "picLicense": "苏EEHC65", "stopNum": 2, "speed": 0.44, "licenseColor": 9, "baseLocation": 22, "originalColor": 3, "courseAngle": 286.1, "id": "400662900", "longitude": 120.6260196 },...]
    读取属性时 需要根据数据格式做修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    // 初始化地图
    map = new mapboxgl.Map({
    container: 'map',
    style: 'http://106.120.201.126:14724/style_zpark.json',
    zoom: 19,
    maxZoom: 22,
    minZoom: 4,
    center: [116.28686, 40.0518],
    pitch: 65,
    bearing: 15,
    })
    // 地图加载完成 -> 创建3D场景 加载模型
    map.on('style.load', () => {
    map.on('click', (e) => {
    console.log(e)
    })
    // 添加3D图层,以支持three模型加载
    map.addLayer({
    id: 'vehicle3D',
    type: 'custom',
    renderingMode: '3d',
    onAdd: (map, mbxContext) => {
    // threebox场景
    window.tb = new Threebox(map, mbxContext, {
    defaultLights: true,
    enableSelectingObjects: false,
    })
    // 加载模型 若为vue项目则对应的模型文件需要放在public目录下,路径省略public即可引入
    window.tb.loadObj(
    {
    obj: `./car0.gltf`,
    type: 'gltf',
    units: 'meters',
    scale: 0.8,
    adjustment: { x: 0.5, y: 1, z: -0.6 },
    cloned: true,
    },
    (model) => {
    vehicleModel = model
    initWs()
    }
    )
    },
    render: function (gl, matrix) {
    window.tb.update()
    },
    })
    })

    let lastLocation = [] // ws上一帧数据
    let vehicleModels = [] // 存放当前地图中的车辆模型
    let currentLocation = [] // ws当前帧数据
    // 初始化websocket
    function initWs() {
    if (typeof WebSocket === undefined) {
    console.error('您的浏览器不支持socket')
    } else {
    socket = new WebSocket(wsURL)
    socket.onopen = (e) => onOpen(e)
    socket.onmessage = (e) => onMsg(e)
    socket.onerror = (e) => onError(e)
    socket.onclose = (e) => onClose(e)
    }
    }
    // websocket连接成功 发送前后端约定好的数据激活推送
    function onOpen(e) {
    console.log('onOpen', e)
    e.target.send(JSON.stringify({ dataType: '1', leftDownXy: '123,33', rightUpXy: '123,33', level: 17 }))
    }
    // 更新模型属性
    function setModel(model, options) {
    // 修改模型材质颜色
    model.traverse((child) => {
    // 根据自己模型材质名称配置,我的模型车辆颜色由name_19材质控制,所以这么写
    if (child.isMesh && child.name.includes("_19")) {
    child.material = child.material.clone();
    child.material.color.set('#000'); // 这里我默认给个黑色,可以根据options中自己推送的数据属性或者字典匹配赋值颜色
    }
    });
    // 加判断防止距离过近模型抖动
    if (
    turf.distance(
    [model.userData.data.longitude, model.userData.data.latitude],
    [options.longitude, options.latitude],
    { units: 'kilometers' }
    ) > 0.0005 ||
    options.dill === 'add'
    ) {
    model.setCoords([options.longitude, options.latitude]) // 设置经纬度
    model.setRotation({ x: 90, y: 360 - options.courseAngle - 90, z: 0 }) // 设置转向角,根据自己推送数据的角度调整计算
    }
    }
    // 根据聚合后的数据处理模型增删改
    function addDelUpdateVehicleModels(allData) {
    // debugger
    for (let item of allData) {
    // 新增
    if (item.dill === 'add') {
    let model = vehicleModel.duplicate()
    setModel(model, item)
    window.tb.add(model)
    vehicleModels.push(model)
    }
    for (let model of vehicleModels) {
    // 删除
    if (item.dill === 'del') {
    if (model.userData.data.id === item.id) {
    // traverse遍历属性删除 ,要不会一直占用内存
    model.traverse((child) => {
    if (child.type === 'Mesh') {
    child.geometry.dispose()
    child.material.dispose()
    }
    })
    window.tb.remove(model)
    }
    }
    // 更新
    if (item.dill === 'com') {
    if (model.userData.data.id === item.id) {
    setModel(model, item)
    }
    }
    }
    }
    }
    // 格式化list为geojson数据
    function formatArray2Geojson(array) {
    let source = {
    type: 'FeatureCollection',
    features: [],
    }
    for (let item of array) {
    source.features.push({
    type: 'Feature',
    properties: item,
    geometry: {
    type: 'Point',
    coordinates: [
    item.longitude || item.x || item.xy.split(',')[0],
    item.latitude || item.y || item.xy.split(',')[1],
    ],
    },
    })
    }
    return source
    }
    // 比较前后两帧数据并聚合
    function diff(oldData, newData) {
    const add = newData.filter((e) => !oldData.find((c) => c.id === e.id))
    const del = oldData.filter((e) => !newData.find((c) => c.id === e.id))
    const com = newData.filter((e) => oldData.find((c) => c.id === e.id))
    add.forEach((a) => (a.dill = 'add'))
    del.forEach((d) => (d.dill = 'del'))
    com.forEach((c) => (c.dill = 'com'))
    return [...add, ...del, ...com]
    }
    // websocket接收消息回调
    function onMsg(data) {
    let dataArray = JSON.parse(data.data)
    let newArray = dataArray.filter((item) => {
    return item.type != 4 && item.type != 5 && item.type != 6
    })
    let geojsonData = formatArray2Geojson(newArray)
    // 每个模型上添加对应的车牌号mapbox 2D图层,并随着推送数据更新
    if (!map.getSource('vehicle_data')) {
    map.addSource('vehicle_data', { type: 'geojson', data: geojsonData })
    } else {
    map.getSource('vehicle_data').setData(geojsonData)
    }
    if (!map.getLayer('idLayer')) {
    map.addLayer({
    id: 'idLayer',
    type: 'symbol',
    source: 'vehicle_data',
    layout: {
    'text-field': ['get', 'id'],
    'text-size': 12,
    'text-allow-overlap': true,
    },
    paint: {
    'text-color': '#fff',
    'text-halo-blur': 1,
    'text-halo-color': '#01309b',
    'text-halo-width': 3,
    },
    })
    map.setLayoutProperty('idLayer', 'visibility', 'visible')
    }
    currentLocation = newArray
    let allData = diff(lastLocation, currentLocation)
    lastLocation = newArray
    addDelUpdateVehicleModels(allData)
    document.getElementById('intervalTime').innerText = newArray[0].dateTime
    }
    // websocket错误...
    function onError(e) {
    console.log('onError', e)
    }
    // websocket关闭...
    function onClose(e) {
    console.log('onClose', e)
    }
  5. 逻辑代码注释已经很完备了,还是不清楚的话对应api可以参考mapboxthreebox文档即可 ^_^
上一篇:
记一次Cesium的property插值用法
下一篇:
Cesium自定义popup封装使用