基于 OpenLayers 实现态势地图手动标绘航线、作战区域及点位悬浮弹窗
需求基于 OpenLayers 技术框架实现地图交互效果支持操作人员在地图上手动绘制飞机运动轨迹、舰船航行轨迹以及作战区域范围当鼠标光标移动悬浮至飞机、舰船点位之上时页面弹出悬浮信息面板展示当前对应装备的详细数据信息。html部分div styleheight: 100vh;width: 100vw el-dropdown split-button sizemini :disabledisDelete stylemargin-right: 10px clickclickDraw {{isDraw ? 结束绘制 : 开始绘制}}{{drawTypeText}} template #dropdown el-dropdown-menu el-dropdown-item click.nativechangeDrawType(plane)飞机/el-dropdown-item el-dropdown-item click.nativechangeDrawType(ship)舰船/el-dropdown-item /el-dropdown-menu /template /el-dropdown el-button :disabled!isDraw sizemini clickclearDraw清除绘制/el-button el-button sizemini :disabledisDraw clickdeleteDraw{{isDelete ? 确认删除 : 删除轨迹}}/el-button !-- 地图容器 -- div idmap classmap/div !-- 地图需要展示的div内容 -- div styledisplay: none div idoverPlay div classoverPlayText v-ifhoverPointInfo.type plane名称飞燕一号/div div classoverPlayText v-ifhoverPointInfo.type ship名称泰坦尼克号/div div classoverPlayText v-ifhoverPointInfo.type plane飞行速度2.25马赫2400公里/小时/div div classoverPlayText v-ifhoverPointInfo.type ship航速40节/div div classoverPlayText v-ifhoverPointInfo.type event在这拐了个弯/div /div /div /div需要定义的数据如下data(){ return { map: null, areaLayer: null, //区域 pointLayer: null, //点 trailLayer: null, //轨迹 pointInfo: {}, //鼠标点击点信息 hoverPointInfo: {}, //鼠标移入点信息 drawLayer: null, //绘图的轨迹 pointTempLayer: null, //绘制轨迹时添加的临时点 isDraw: false, //是否正在绘制 isDelete: false, //是否在删除 clickPoints: [], //绘制时暂存的点 tempLine: null, //临时边 tempPoint: null, //临时点 drawType: , //地图绘制类型 } }, computed: { drawTypeText(){ let text let map { plane: 飞机, ship: 船舰, } if(this.drawType){ text map[this.drawType] } return (text ? (( text )) : ) } },初始化地图此方法完成地图实例创建、各类矢量图层初始化、地图交互事件绑定加载基础底图资源同时注册点位选中、鼠标悬浮监听逻辑。initMap(){ this.map null this.layer null this.pointLayer null this.trailLayer null // 图层 this.layer new TileLayer({ source: new XYZ({ visible: true, url: http://webrd01.is.autonavi.com/appmaptile?x{x}y{y}z{z}langzh_cnsize2scale1style8, wrapX: true, }), }) //区域 this.areaLayer new Vector({ source: new VectorSource(), }) //点 this.pointLayer new Vector({ source: new VectorSource(), }) //轨迹 this.trailLayer new Vector({ source: new VectorSource(), }) //临时点 this.pointTempLayer new Vector({ source: new VectorSource(), }) // 初始化地图到指定DOM元素 this.map new Map({ layers: [this.layer, this.trailLayer, this.areaLayer, this.pointLayer, this.pointTempLayer], target: map, view: new View({ projection: EPSG:4326, center: [135.403218, 30.92372], zoom: 4, maxZoom: 11, constrainResolution: true, // 设置缩放级别为整数 smoothResolutionConstraint: false, // 关闭无级缩放地图 }), }); //鼠标点击点图层获取该点的数据 let selectInteraction new Select({ layers: [this.pointLayer], }); this.map.addInteraction(selectInteraction) selectInteraction.on(select, e { const selectedFeature (e.selected || [])[0]; if(selectedFeature){ this.pointInfo selectedFeature.getProperties() console.log(this.pointInfo) } }) //鼠标移入显示id为overPlay的div let hoverFeature null this.map.on(pointermove,(e) { let feature this.map.forEachFeatureAtPixel(e.pixel, (feature, layer) { return feature },{hitTolerance: 5}) //有feature且feature变化才执行 if(feature ! hoverFeature){ hoverFeature feature if(feature feature.getGeometry().getType() Point){ this.map.getTargetElement().style.cursor pointer; this.hoverPointInfo feature.getProperties() this.addOverlay(this.hoverPointInfo.coords,overPlay) }else{ this.map.getTargetElement().style.cursor ; this.hoverPointInfo {} this.removeOverlays() } } }) },数据上图模拟接口返回数据模拟接口返回地理数据将作战区域、飞机点位、舰船点位、运动轨迹、轨迹拐点批量渲染至地图上。setTimeout(() { // //区域 let data1 [ [130.403218, 10.92372], [138.403218, 8.92372], [140.403218, 15.92372], [135.403218, 16.92372], ] let data2 [ [160.403218, 24.92372], [165.403218, 29.92372], [164.403218, 32.92372], [160.403218, 27.92372], ] this.addArea([data1,data2]) let data3 [ { id: 5, type: plane, coords: [177.403218, 27.92372], }, { id: 6, type: plane, coords: [120.403218, 12.92372], }, ] data3.forEach(e { this.addPoint(plane,e) }) let data4 [ { id: 7, type: ship, coords: [165.403218, 28.92372], }, { id: 8, type: ship, coords: [138.403218, 14.92372], }, ] data4.forEach(e { this.addPoint(ship,e) }) let data5 [ [ [177.403218, 27.92372], [150.403218, 10.92372], [120.403218, 12.92372], ], [ [165.403218, 28.92372], [138.403218, 14.92372], ] ] this.addTrail(red,data5) this.addPoint(red,{ id: 10, type: event, coords: [150.403218, 10.92372], }) // this.addHitArea(8000,1500) },2000)地图上加区域记录用户在绘制时点击地图点的位置在地图的相应图层绘制成多边形。//地图上加区域 addArea(data){ let feature new Feature({ geometry: new Polygon(data), }) feature.setStyle(mapStyle.redAreaStyle) this.areaLayer.getSource().addFeature(feature) },地图上加点根据点位类型区分飞机、舰船、轨迹拐点加载不同样式将点位渲染至点位图层。//地图上加点 addPoint(type, data){ if(type plane){ let point new Point(data.coords) let pointFeature new Feature({ geometry: point, ...data }) pointFeature.setStyle(mapStyle.pointRedPlane) this.pointLayer.getSource().addFeature(pointFeature) } if(type ship){ let point new Point(data.coords) let pointFeature new Feature({ geometry: point, ...data }) pointFeature.setStyle(mapStyle.pointRedShip) this.pointLayer.getSource().addFeature(pointFeature) } if(type red){ let point new Point(data.coords) let pointFeature new Feature({ geometry: point, ...data }) pointFeature.setStyle(mapStyle.pointRed) this.pointLayer.getSource().addFeature(pointFeature) } },地图上加轨迹记录用户在绘制时点击地图点的位置在地图的相应图层连点成线。//地图上加轨迹 addTrail(type,data){ data.forEach(e { let feature new Feature({ geometry: new LineString(e), }) feature.setStyle(mapStyle.redLineArrow(feature)) this.trailLayer.getSource().addFeature(feature) }) },地图上加div调用addOverlay方法后会创建dom避免出现过多无用dom情况下次新增时只改变overlay的位置。//地图上加div addOverlay(data,id){ if(!this.map.getOverlayById(id)){ let marker new Overlay({ id, position: data, element: document.getElementById(id), offset: [10, 20] }); this.map.addOverlay(marker); marker.setPositioning(top-left); } else{ let marker this.map.getOverlayById(id) marker.setPosition(data) } },地图上删除div使用插件的api删除overlay后再次调用addOverlay方法新增overlayoverlay显示异常。换个思路用户想要删除overlay时为overlay设置位置为 undefined 隐藏overlay。//地图上删除div removeOverlays() { let overlays this.map.getOverlays(); for (let i 0, len overlays.getLength(); i len; i) { overlays.item(i).setPosition(undefined) } },手动绘制轨迹点击开启绘制按钮后为地图绑定点击监听事件操作人员点击地图采集坐标生成临时点位当采集点位数量不少于两个时自动连线生成临时轨迹。点击结束绘制按钮之后移除地图点击事件将临时点位、临时线路转换为正式持久化的点位要素与轨迹线路。//修改绘制类型 changeDrawType(type){ this.drawType type }, //开始绘制地图 clickDraw(){ if(this.isDraw){ this.endDraw() }else{ if (!this.drawType) { this.$message.warning(请选择绘制类型) return } this.drawTrail() } }, //进入开始手动绘制状态 drawTrail(){ if(!this.isDraw){ this.isDraw true this.clickPoints [] this.map.on(click, this.handleMapClick) } }, //结束手动绘制状态 endDraw(){ if(this.isDraw){ this.isDraw false this.map.un(click, this.handleMapClick) //唯一标识,便于统一管理 let groupId point- Date.now() //临时点改为真实点 if(this.clickPoints){ this.pointTempLayer.getSource().clear() this.tempPoint null this.clickPoints.forEach((e, index) { if(index 0 || index (this.clickPoints.length - 1)){ this.addPoint(this.drawType, { type: this.drawType, coords: e, groupId }) } }) } //临时线改为真实线 if(this.clickPoints.length 2){ let feature new Feature({ geometry: new LineString(this.clickPoints), groupId }) feature.setStyle(mapStyle.redLineArrow(feature)) this.trailLayer.getSource().addFeature(feature) if(this.tempLine){ this.trailLayer.getSource().removeFeature(this.tempLine) this.tempLine null } this.clickPoints [] } } }, //绘制的点击事件 handleMapClick(e){ if(!this.isDraw) return; const coord e.coordinate this.clickPoints.push(coord) if(this.tempLine){ this.trailLayer.getSource().removeFeature(this.tempLine) } if(this.clickPoints.length 2){ this.tempLine new Feature({ geometry: new LineString(this.clickPoints), }) this.tempLine.setStyle(mapStyle.tempLineArrow(this.tempLine)) this.trailLayer.getSource().addFeature(this.tempLine) } if(coord){ this.tempPoint new Feature({ geometry: new Point(coord) }) this.tempPoint.setStyle(mapStyle.positionPointTemp) this.pointTempLayer.getSource().addFeature(this.tempPoint) } }清除绘制清空绘制过程中产生的所有临时点位与临时线路重置坐标集合用于绘制中途放弃时清理画布临时要素。//清除绘制 clearDraw(){ this.pointTempLayer.getSource().clear() this.tempPoint null if(this.tempLine){ this.trailLayer.getSource().removeFeature(this.tempLine) this.tempLine null } this.clickPoints [] },删除绘制内容手动绘制生成的点位、轨迹线路会绑定 groupId 属性作为唯一分组标识系统仅允许删除带有 groupId 属性的手绘要素预加载的静态轨迹无法执行删除操作。点击删除模式后点击地图对应轨迹即可删除同一分组下全部点位与线路。//删除轨迹 deleteDraw(){ this.isDelete !this.isDelete if(this.isDelete){ this.map.on(click,this.handleMapDelete) }else{ this.map.un(click,this.handleMapDelete) } }, handleMapDelete(e){ let feature this.map.forEachFeatureAtPixel(e.pixel, (feature, layer) { return feature },{hitTolerance: 5}) if(!feature.get(groupId)){ this.$message.warning(该轨迹非手绘轨迹,不可删除) return } const pointFeatures this.pointLayer.getSource().getFeatures() pointFeatures.forEach(f { if(f.get(groupId) feature.get(groupId)){ this.pointLayer.getSource().removeFeature(f) } }) const lineFeatures this.trailLayer.getSource().getFeatures() lineFeatures.forEach(f { if(f.get(groupId) feature.get(groupId)){ this.trailLayer.getSource().removeFeature(f) } }) },样式统一封装地图各类矢量要素渲染样式包含作战区域填充样式、飞机图标、舰船图标、轨迹线段、临时绘制要素样式集中管理方便后期统一调整可视化效果。import Fill from ol/style/fill; import Stroke from ol/style/stroke; import Style from ol/style/style; import Icon from ol/style/icon; import Circle from ol/style/circle; import Point from ol/geom/point; //红色多边形样式 function redAreaStyle(){ let fillColor new Fill({ color: rgba(255, 0, 0, 0.2) }) let stroke new Stroke({ color: red, width: 1, }) return new Style({ stroke, fillColor }) } //红色船 function pointRedPlane(){ return new Style({ image: new Icon({ src: require(../img/redPlane.svg), scale: 0.2, //缩放比例 }) }) } //红色飞机 function pointRedShip(){ return new Style({ image: new Icon({ src: require(../img/redShip.svg), scale: 0.2, //缩放比例 }) }) } //红色点 function pointRed(){ return new Style({ image: new Circle({ radius: 5, fill: new Fill({ color: red }) }) }) } //红色轨迹线 function redLineArrow(feature) { let geometry feature.getGeometry(); let styles [new Style({ stroke: new Stroke({ color: #d71106, width: 2, lineDash: [6,5] }) })]; geometry.forEachSegment(function (start, end) { let dx end[0] - start[0]; let dy end[1] - start[1]; let rotation Math.atan2(dy, dx); const kx (end[0] start[0]) / 2 const ky (end[1] start[1]) / 2 console.log(kx,ky) //arrows styles.push(new Style({ geometry: new Point([kx, ky]), image: new Icon({ src: require(../img/arrow_red.png), anchor: [0.75, 0.5], rotateWithView: false, rotation: -rotation }) })); }); return styles } //临时箭头 function tempLineArrow(feature) { let geometry feature.getGeometry(); let styles [new Style({ stroke: new Stroke({ color: #ffcc33, width: 2, lineDash: [6,5] }) })]; geometry.forEachSegment(function (start, end) { let dx end[0] - start[0]; let dy end[1] - start[1]; let rotation Math.atan2(dy, dx); const kx (end[0] start[0]) / 2 const ky (end[1] start[1]) / 2 //arrows styles.push(new Style({ geometry: new Point([kx, ky]), image: new Icon({ src: require(../img/arrow.png), anchor: [0.75, 0.5], rotateWithView: false, rotation: -rotation }) })); }); return styles; } //临时点 function positionPointTemp(){ return new Style({ image: new Circle({ radius:5, fill:new Fill({ color: #ffcc33 }) }), }); } let mapStyle { redAreaStyle, pointRedPlane, pointRedShip, pointRed, redLineArrow, positionPointTemp, tempLineArrow, } export default mapStyle项目地址

相关新闻

C++进阶(11):C++ IO 流

C++进阶(11):C++ IO 流

💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分…

2026/10/14 1:29:01 阅读更多 →
3分钟搞定iPhone USB网络共享驱动安装:Windows用户的终极救星

3分钟搞定iPhone USB网络共享驱动安装:Windows用户的终极救星

3分钟搞定iPhone USB网络共享驱动安装:Windows用户的终极救星 【免费下载链接】Apple-Mobile-Drivers-Installer Powershell script to easily install Apple USB and Mobile Device Ethernet (USB Tethering) drivers on Windows! 项目地址: https://gitcode.com…

2026/10/8 11:55:06 阅读更多 →
【Easylive】consumes = MediaType.MULTIPART_FORM_DATA_VALUE 与 @RequestPart

【Easylive】consumes = MediaType.MULTIPART_FORM_DATA_VALUE 与 @RequestPart

【Easylive】项目常见问题解答(自用&持续更新中…) 汇总版 consumes MediaType.MULTIPART_FORM_DATA_VALUE 的作用 1. 定义请求的数据格式 • 作用:告诉 Feign 和 HTTP 客户端,这个接口 接收的是 multipart/form-data 格式的…

2026/10/8 19:13:39 阅读更多 →

最新新闻

Qt跨数据库获取SQL表字段元数据的工程实践

Qt跨数据库获取SQL表字段元数据的工程实践

1. 项目概述:为什么“获取SQL数据表所有字段”是Qt开发中绕不开的硬需求在用Qt做数据库应用开发时,我几乎每天都会遇到这个场景:界面刚打开,需要动态加载某张表的全部字段名来构建查询条件面板;或者用户点开一个数据编…

2026/10/15 13:01:26 阅读更多 →
页面没有报截断,短屏上确认按钮却消失了:HarmonyOS 7 怎么验关键任务始终可达

页面没有报截断,短屏上确认按钮却消失了:HarmonyOS 7 怎么验关键任务始终可达

页面没有报截断,短屏上确认按钮却消失了:HarmonyOS 7 怎么验关键任务始终可达 DevEco Studio 26.0 场景化体检把“截断”纳入多设备检测,官方多设备指南也明确提醒短屏高度受限时要保证关键区可见并支持滚动。但最难发现的不是文字少一行&am…

2026/10/15 13:01:26 阅读更多 →
AI 卖课博主年入 120 万?程序员用 Cursor 算完这笔账,我沉默了

AI 卖课博主年入 120 万?程序员用 Cursor 算完这笔账,我沉默了

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 13:01:26 阅读更多 →
RetroArch BIOS 资源包:一键补齐多平台模拟器 BIOS 文件

RetroArch BIOS 资源包:一键补齐多平台模拟器 BIOS 文件

简介:这份资源面向复古游戏模拟器玩家与 Retropie、Recalbox、Lakka、Retrobat、EmuVR 等前端使用者,集中提供 libretro 体系所需的系统固件与 BIOS 文件,解决模拟器因缺少底层固件而无法启动或运行异常的问题。压缩包共 262 个文件&#xff…

2026/10/15 13:01:26 阅读更多 →
深入理解context-mode:给AI划边界,让编程更稳定、更省token

深入理解context-mode:给AI划边界,让编程更稳定、更省token

其实最开始我对context-mode这词挺不以为然,觉得不就是“多给AI塞点资料”吗?直到在项目里被坑了好几次,才意识到这观念错得离谱。有一阵子我习惯把项目里相关的文件全部丢给AI代码助手,结果它给的修改建议经常自相矛盾&#xff0…

2026/10/15 13:01:26 阅读更多 →
人脸特征提取三路线:HoG、Dlib与CNN原理及选型指南

人脸特征提取三路线:HoG、Dlib与CNN原理及选型指南

人脸识别这几年从实验室走向了门禁、考勤、相册聚类、甚至手机解锁,几乎成了计算机视觉里最"接地气"的方向之一。但真到动手做项目的时候,很多人会卡在同一个地方:特征到底怎么提?我见过不少朋友一上来就堆深度学习模型…

2026/10/15 13:00:25 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →