结合SSE实现实时位置展示与轨迹展示

news2024/12/22 23:13:12

概述

实时位置与实时轨迹的展示是webgis中非常常见的一个功能,本文结合SSE来实现实现此功能。

SSE简介

SSE是Sever-Sent Event的首字母缩写,它是基于HTTP协议的,在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包,而是text/event-stream类型的数据流信息,在有数据变更时从服务器流式传输到客户端。

image.png

websockSSE都可以实现在有数据变更时从服务器主动推送到到客户端,他们的比较如下:

  • SSE 是基于HTTP协议的,它们不需要特殊的协议或服务器实现即可工作;WebSocket需单独服务器来处理协议。
  • SSE 单向通信,只能由服务端向客户端单向通信;webSocket全双工通信,即通信的双方可以同时发送和接受信息。
  • SSE 实现简单开发成本低,无需引入其他组件;WebSocket传输数据需做二次解析,开发门槛高一些。
  • SSE 默认支持断线重连;WebSocket则需要自己实现。
  • SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket默认支持传送二进制数据。

由于SSE 单向通信的特性,所以很适合“实时位置与实时轨迹的展示”这样的场景。

实现效果

image.png

实现代码

服务端代码

服务端使用Node通过expressstream实现SSE,实现代码如下:

const express = require('express')
const stream = require("stream");

const app = express()
let sse = null, contentId = 0

function toDataString(data) {
  if (typeof data === 'object') return toDataString(JSON.stringify(data));
  return data.split(/\r\n|\r|\n/).map(line => `data: ${line}\n`).join('');
}

function random(n, m, is = false) {
  let res = Math.random() * (m - n) + n
  const arr = [-res, res]
  if(is) res = arr[Math.round(Math.random())]
  return res
}

let carDict = {}
for (let i = 0; i < 20; i++) {
  const [xmin, ymin, xmax, ymax] = [114.0422270397205295, 22.5261218968098547, 114.0854819347529912, 22.5488829187520494]
  carDict['car'+i] = {
    id: 'car'+i,
    lon: random(xmin, xmax),
    lat: random(ymin, ymax),
    dx: random(0.0001, 0.0005, true),
    dy: random(0.0001, 0.0005, true)
  }
}

app.use(express.static(__dirname + '/web'))

app.get("/sse", (req, res) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream; charset=utf-8"
  });
  sse = new stream.Transform({ objectMode: true });

  sse._transform = (message, encoding, callback) => {
    if (message.comment) sse.push(`: ${message.comment}\n`);
    if (message.event) sse.push(`event: ${message.event}\n`);
    if (message.id) sse.push(`id: ${message.id}\n`);
    if (message.retry) sse.push(`retry: ${message.retry}\n`);
    if (message.data) sse.push(toDataString(message.data));
    sse.push("\n");
    callback();
  };
  sse.write(':ok\n\n');
  sse.pipe(res);
});

// 触动定时触发
setInterval(() => {
  for (const carid in carDict) {
    const {dx, dy} = carDict[carid]
    carDict[carid].lon += dx
    carDict[carid].lat += dy
  }
  const message = {
    data: carDict,
    event: "dynamicUpdate", // 事件类型,需要客户端添加对应的事件监听
    id: ++contentId,
    retry: 2000,
  };
  sse?.write(message);
}, 1000)

app.listen(18888, () => {
  console.log('express server running at http://127.0.0.1:18888')
})

客户端代码

<!DOCTYPE html>
<html>
<head>
  <title>XYZ</title>
  <meta charset="utf-8">
  <link rel="stylesheet" href="https://openlayers.org/en/v4.6.5/css/ol.css" type="text/css">
  <style>
      #map,
      body,
      html {
          margin: 0;
          padding: 0;
          width: 100%;
          height: 100%;
          overflow: hidden;
          font-size: 16px;
      }
      .tools {
          position: absolute;
          top: 20px;
          right: 20px;
          z-index: 99;
      }
  </style>
  <script src="https://openlayers.org/en/v4.6.5/build/ol.js"></script>
</head>
<body>
<div id="map" class="map">
  <div class="tools">
    <button id="start">start</button>
    <button id="stop">stop</button>
  </div>
</div>
<script>
  let es, map
  const navLayer = new ol.layer.Tile({
    source: new ol.source.XYZ({
      url: 'http://webrd01.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8'
    })
  });
  const styleFunction = (feat) => {
    const rotation = feat.get('rotation')
    const stroke = new ol.style.Stroke({
      color: '#04991f',
      width: 5
    });
    const imageData = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAACJRJREFUeF7tWXtQU1ce/k54yCMRqQ8IQUEQwRcgwYovqLKaIOjqdB1odaxVV3Rnytqx1and8bHbdUsru7q2K7C16nSZqtvdnVaFgPVB6EIVggiiCCKI8vKByCsQQs7OvRBKhHBDYpfMkPNX7j2/x3e+8/1+99wbghE+yAhfPywEWBQwwhmwlMAIF4ClCVpKwFICI5wBSwmMcAFYngKWErCUwAhnwFIC5iKAgqKiZTzgAy2eLo0mc3ZAwL6fG5/ZKODGzZupoDSid8GE1DRbW/st9PNr/jlJMBcC7JO/+OKRFY/H77vY7y9efPfrlJRDZk8AzRAuQxedbyzQksfjpp/OqFzzon/wrMmlkf4NG0hEdY6xsbn8TFIATROFg6dJBoUXVyKT5gmtAchJIqndbVKcAZyNJoCmu0wA5WUC8HvZoPTHo8eJtG7jy8xnPAEy4V8AbNeCOX7ZGhvjHwyKTejihJqTDqzNtsQunEhvQruynXM95w6IEBmk0dolEGnte5xOBhqYQsB1AIHaPP7vqKAW+MKBP1pv6qf11ag4/ISdj00ehZyHHrC1s+eE6qS5j4+jn2KOj1W3LeGFE0n1JU5HAwyMIoCeFwXCSsMQwI57dV347b9mova5LWfKvN03WJvkLBckZ7ly2msNYuY8waZ5D+DM5wGgF4m07hcGOw9iaBwBaW5xIPSwNu7dx3aI+buvQXiMJYAJvn9FFSJnPetRAYkjkpojBiV9+QQIvwLBOm1cRRWf3VGuMdOtDe8srmXNsssFOKMYB2Uns6OGDfGkFmxZVK81bgC/05MsfGLSQYlTATTVdTwIZuhAJOTo/7f76yUoEZSeHnCWah6Q5Y/KuagdlACa5roGhJzhCmK284SeJZK6lYPh00sATXdbCkozTFncthRv1v3ooXg0d1jjvV07sGXDWogD/bFt+y4IRZOx5/1YFN2uwGdJSTgYnwDBKDVit+9i/ZLWcm4gNzxK95OIOr0vVfoJSBVuBw/Ms17vKL6vhiyvA9fuqCCw52HeNBusCLHDhDHddb3pH9NR1WCLC+dT0Vx7FYtj9iFp7y8hXrIVK1dJ4ec9EZ8cPIrC7FPYuDsFl0/tg0A4F8FhERB7tOgQcPmGCpcKOlBYoYaX0Aphs2yxar4dNwGE1hBJnUifoV4Clp3751U+Ub7KOL4fFIaQuo+A+nNsnB9uqnDgdAvS89VwcBTA3lEAjUaDtpYmKFubsWW5A/a8yYcoSgY4eiM4PBpvSHywY/dfobiUiNj93yIv4zhU7c2YvzIOn/1uNUKWbkHCgTh8nV6GvMw0oCEbyP0VTn6vxIaDjbCxHQUHgRN7zmhrfg5lWwtU7UoceFuAD6L7vEONC4NC9EccLspFa6eKxXtYk+A+ccWN6oFIGJSADBrCEqCJFIGUfAhUncCJC0q8ndAIvpMzPKbOhJ29o07clufPUHmnCDZEierbaRjtwEPsjt9jqt9s7NgaA0VBIZJPpCApYQ/aVDx8uHcPoqPfQoh4JhIST6G05DqSDsWzMbetX4rE820Y5+rO5npxNDyqQUVJIauGK5+O7Z4Wrgb8P4dzeg0aO7tPj4nkT69vjfrbv40mgEa5A7d3o+LaMXhteAShxxS4eUwZVH63FP+FyEmJsi/Hc8t0AIuUS0qs+6QR3jOCMGbsBL0x1OpO3Mi+iJ1r+IjfJBiQgP0k6Q97oz7aYxAB4lDp56CYbuvv4zd2VIerr4sS614TIbDpG2w+WIP/5Dth8rQAnVhuQlfweAQPq7uf8czoVHWgODcLSXGO2CTpPv8PZRBpLdy9/ODi7tnr5jnJHQI+H8qODlRU3kdXV/cOP659gKqyYtw5Nh5TRTYocF6PxoYiZGRpUF4FPGuzqm54rC4jPOTmZcp29sXRrwTEoVLa10DbjB41auASUw/fwLngj3ZmTXyneMHP1wdenpPYa4aA1AuX0Nraxl7XP6wEaS5D1Vf6d3AgUs7I27H201YEzFvSO/36yuWYNPGnXva8qRnffHseTU3d56B7t67j14tbu1XQM2JTvKG4r/ONBQq5TGfNBhPwZXobNh9qwuyFS0FId5dfFSnB5J7Fa5PmXFPgx9x89lLV9gxFeVdRemw8fETWBgvgjY8bca3CEc4ewazP1CleiJSE9/PPu16IrOyr7P3WJxVQN5brlJxRBASFSq8RYI42GwHO5sll7GFCvEgSBEIUzO9XnMfgrTf7fcRhd59pcj2jWSGX6X89HISS4DDpu5Tiz4zJq+JALAjphdTrVV5Rie9SL2ivzyvksqi+IdlyBn7T516lQi6b3NemnwICX5N6WqvRW3hdhBRfz0p7zDjNDV/pou5U1WkDbFofg9GCnyTH3L97rxJn03pAERQrMmX927cBWggKk64mFGznDpg5HUvCFvTzulVSivSLzDcZMP/xJefJZbF9jWYvihhvRWnvMV5tjcqCK7LKQQngwiYOlTLvs/6M3XS/qZCEh+m4HEk6DrVarb13TCGXbeaKOdC8eH6EN6zpXWbOabQAG9ZFg0d094shmiG8h4DNeXLZsaHm4nwZejFgUKh0HwH2au87j3HCrBnToGxvR66iQMecB82CXHlG9lBBae2DQ6XfUWAFcy1yc4X/jGms4phcpWXlKCnrOSoTFLeT9pDiK1dahppryAQwCcShUuZU5TZYMgrsz5fLTPpjY948ySsqG/KUa1GmEG0UAQygF5WgA5LiiCJLFscF3JD5nr7DfPjo33GBGgqyM1+e1tt1DYlpUg/o6ywOjVgGSsWE0ABKiBIaepVSmpv/Qwb7pHiZQxwmXQNK5jL5QPAjQItsOyHLyUlvMCWP0QowJak5+VoIMKfdGA4sFgUMB+vmlNOiAHPajeHAYlHAcLBuTjktCjCn3RgOLBYFDAfr5pTTogBz2o3hwGJRwHCwbk45R7wC/gcdjfxfQwrgNAAAAABJRU5ErkJggg=='
    let style = {}
    if(rotation) {
      const carId = feat.get('carId')
      style = {
        image: new ol.style.Icon({
          src: imageData,
          rotation: rotation
        }),
      }
    } else {
      style = {
        stroke
      }
    }
    return new ol.style.Style(style)
  }
  let vectorSource = new ol.source.Vector({
    features: []
  })
  let vectorLayer = new ol.layer.Vector({
    source: vectorSource,
    style: styleFunction
  });
  map = new ol.Map({
    target: 'map',
    layers: [ navLayer, vectorLayer ],
    view: new ol.View({
      minZoom: 0,
      maxZoom: 18,
      center: [132921716.5468307, 2575606.3396163424],
      zoom: 14
    })
  });

  let carFeature = {},
    carCoords = {},
    carRoute = {};

  function getRotation([x1, y1], [x2, y2]) {
    let res = Math.atan2(y1 - y2, x1 - x2)
    res = y2 > y1 ? res + Math.PI / 2 : res - Math.PI / 2
    return res
  }

  const openServerPush = () => {
    es = new EventSource("/sse");
    es.addEventListener("dynamicUpdate", (e) => {
      let carsDara = JSON.parse(e.data)
      for (const carId in carsDara) {
        const {lon, lat} = carsDara[carId]
        const coord = ol.proj.fromLonLat([lon, lat])

        if(!carCoords[carId]) carCoords[carId] = []
        carCoords[carId].push(coord)

        if(carCoords[carId].length > 1) {
          const geomLine = new ol.geom.LineString(carCoords[carId])
          if(carRoute[carId]) {
            carRoute[carId].setGeometry(geomLine)
          } else {
            const feature = new ol.Feature({
              geometry: geomLine,
              carId
            });
            vectorSource.addFeature(feature)
            carRoute[carId] = feature
          }
        }

        const geom = new ol.geom.Point(coord)
        if(carFeature[carId]) {
          carFeature[carId].setGeometry(geom)
          const prevCoord = carCoords[carId][carCoords[carId].length - 2]
          carFeature[carId].set('rotation', getRotation(prevCoord, coord))
        } else {
          const feature = new ol.Feature({
            geometry: geom,
            carId,
            rotation: 0
          });
          vectorSource.addFeature(feature)
          carFeature[carId] = feature
        }
      }
    });
    es.onopen = () => {
      console.log("已开启。。。");
    };
    es.onmessage = (e, me) => {
      console.log("默认推送:" + e.data);
    };
    es.onerror = (err) => {
      console.log(err);
    };
  };

  const closeServerPush = () => {
    if (es) {
      es.close();
    }
  };

  document.getElementById('start').onclick = openServerPush
  document.getElementById('stop').onclick = closeServerPush
</script>
</body>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/493364.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

车牌输入框 封装 (小程序 vue)

车牌输入框 封装 小程序licenseNumber.jslicenseNumber.jsonlicenseNumber.wxmllicenseNumber.wxss样例 vuevnp-input-box.vuevnp-input.vuevnp-keyboard.vue样例 小程序 licenseNumber.js const INPUT_NUM 8;//车牌号输入框个数 const EmptyArray new Array(INPUT_NUM).fi…

6个「会议议程」实例和免费模板

我们都参加过一些团队会议&#xff0c;在这些会议上&#xff0c;大多数与会者对会议的目的一无所知&#xff0c;而发言者则使讨论偏离轨道。 接下来就是一场真正的灾难了。 你会发现你的团队因为“上述会议”而浪费了很多时间&#xff0c;却没有达到任何目的。 好消息! 一个…

【Python】序列类型②-元组

文章目录 1.元组简介2.元组的定义2.1定义只有一个元素的元组 3.元组的下标访问4.元组的常用方法5.使用in判断是否存在元素6.多元赋值操作 1.元组简介 元组和列表一样可以存放多个,不同数据类型的元素 与列表最大的不同就是:列表是可变的,而元组不可变 2.元组的定义 元组的定义:…

TCP/UDP协议

一、协议的概念 什么是协议&#xff1f; 从应用的角度出发&#xff0c;协议可理解为“规则”&#xff0c;是数据传输和数据的解释的规则。 假设&#xff0c;A、B双方欲传输文件。规定&#xff1a; 第一次&#xff0c;传输文件名&#xff0c;接收方接收到文件名&#xff0c;…

Springboot +Flowable,ReceiveTask的简单使用方法

一.简介 ReceiveTask&#xff08;接受任务&#xff09;&#xff0c;它的图标如下图所示&#xff1a; ReceiveTask 可以算是 Flowable 中最简单的一种任务&#xff0c;当该任务到达的时候&#xff0c;它不做任何逻辑&#xff0c;而是被动地等待用户确认。 ReceiveTask 往往适…

RepVGG: Making VGG-style ConvNets Great Again

文章地址&#xff1a;《RepVGG: Making VGG-style ConvNets Great Again》 代码地址&#xff1a;https://github.com/megvii-model/RepVGG 文章发表于CVPR2021&#xff0c;文章提出一种将训练态和推断态网络结构解耦的方法。文章认为目前复杂的网络结构能够获取更高的精度&am…

学大数据需要java学到什么程度

大数据需求越来越多&#xff0c;只有技术在手不愁找不到工作。 学习大数据需要掌握什么语言基础&#xff1f; 1、Java基础 大数据框架90%以上都是使用Java开发语言&#xff0c;所以如果要学习大数据技术&#xff0c;首先要掌握Java基础语法以及JavaEE方向的相关知识。 2、My…

记一次OJ在线代码编辑器(代码编译+运行,C、C++、Java)

如何在SpringBootVue的项目中实现在线代码编译及执行&#xff08;支持编译运行C、C、Java&#xff09;&#xff0c;研究了一天&#xff0c;真实能用&#xff0c;下面直接上源码&#xff01;&#xff01;&#xff01; ————————————————————————————…

MySQL 知识:迁移数据目录到其他路径

一、系统环境 操作系统&#xff1a;Centos 7 已安装环境&#xff1a;MySQL 8.0.26 二、开始操作 2.1 关闭SELinux 为了提高 Linux 系统的安全性&#xff0c;在 Linux 上通常会使用 SELinux 或 AppArmor 实现强制访问控制&#xff08;Mandatory Access Control MAC&#xff…

中间件的概念

中间件(middleware)是基础软件的一大类&#xff0c;属于可复用的软件范畴。中间件在操作系统软件&#xff0c;网络和数据库之上&#xff0c;应用软件之下&#xff0c;总的作用是为处于自己上层的应用软件提供运行于开发的环境&#xff0c;帮助用户灵活、高效的开发和集成复杂的…

阶段二38_面向对象高级_网络编程[UDP单播组播广播代码实现]

知识&#xff1a; InetAddresss:getByName,getHostName,getHostAddress方法UDP通信程序&#xff1a;单播&#xff0c;组播&#xff0c;广播代码实现一.InetAddress 的使用 1.static InetAddress getByName(String host) 确定主机名称的IP地址。主机名称可以是机器名称&#x…

【Java】通过反射方法不改变HashCode以修改String的值

如何修改String的值&#xff1f; 我们首先会想到如下两种方法 方式一&#xff1a;通过StringBuild/StringBuffer String s1 "Hello World!"; System.out.println("s1"s1" HashCode"s1.hashCode()); StringBuilder sb new StringBuilder(s1…

Android JNI配置CMakeLists.txt修改.cpp在logcat打印日志

Android JNI配置CMakeLists.txt修改.cpp在logcat打印日志 C/C代码里面常用的printf没法在Android 的logcat输出显示。需要特别配置C才能显示在logcat里面。 &#xff08;1&#xff09;CMakeLists.txt定义&#xff1a; find_library( # Sets the name of the path variable.l…

yolov1原理

目标检测方法 传统的方法可以按照检测系统分为两种&#xff1a; DPM&#xff0c;Deformatable Parts Models&#xff0c;采用sliding window检测R-CNN、Fast R-CNN。采用region proposal的方法&#xff0c;生成一些可能包含待检测物体的potential bounding box&#xff0c;再…

opencv_c++学习(三)

一、获取图像像素指针 CV Assert(mylmage.depth() CV 8U); CV_Assert()函数判断图像数据的类型是否为uchar类型&#xff0c;不满足则抛出异常。 Mat.ptr(int i0)获取像素矩阵的指针&#xff0c;索引i表示第几行&#xff0c;从0开始计行数。 Mat.ptr(int i0)获取像素矩阵的指针…

【五一创作】【远程工具】- Tabby 下载、安装、使用、配置【ssh/Serial】-免安装、解压即用

目录 一、Tabby 概述 二、Tabby 下载、安装 三、Tabby 的使用  &#x1f449;3.1 使用SSH协议连接Linux开发主机  &#x1f449;3.2 使用Serial(串口)协议连接开发板 一、Tabby 概述 在远程终端工具中&#xff0c;secureCrt 和 XShell 是两款比较有名的远程工具&#xff0c;但…

shell脚本之例题详解

文章目录 1 检查用户家目录中的test.sh文件是否存在&#xff0c;并且检查是否有执行权限2 提示用户输入100米赛跑的秒数&#xff0c;要求判断秒数大于0且小于等于10秒的进入选拔赛&#xff0c;大于10秒的都淘汰&#xff0c;如果输入其它字符则提示重新输入&#xff1b;进入选拔…

Selenium:集成测试报告

目录 一、分拆后的实现代码 二、创建用于执行所有用例的ALL_HTMLtest.py文件 三、集成测试报告 随着软件不断迭代功能越来越多&#xff0c;对应的测试用例也会呈指数增长。一个实现几十个功能的项目&#xff0c;对应的用例可能有上百个甚至更多&#xff0c;如果全部集成在一…

RocketMQ中单消费者订阅多个Topic,会阻塞消费吗?

RocketMQ 问题 背景是这样&#xff1a; 最近有个项目用MQ做消息流转&#xff0c;在RocketMQ集群模式下&#xff0c;一个消费者实例&#xff0c;订阅了两个Topic A、B。 Topic A&#xff1a;存储的是批量业务消息。 Topic B&#xff1a;存储的是单个业务消息。 有个小伙伴问我…

基于C++的职工管理系统

1、管理系统需求 职工管理系统可以用来管理公司内所有员工的信息 本教程主要利用C++来实现一个基于多态的职工管理系统 公司中职工分为三类:普通员工、经理、老板,显示信息时,需要显示职工编号、职工姓名、职工岗位、以及职责 普通员工职责:完成经理交给的任务 经理职责:完成…