需求:基于 OpenLayers 技术框架实现地图交互效果,支持操作人员在地图上手动绘制飞机运动轨迹、舰船航行轨迹以及作战区域范围;当鼠标光标移动悬浮至飞机、舰船点位之上时,页面弹出悬浮信息面板,展示当前对应装备的详细数据信息。
html部分
<div style="height: 100vh;width: 100vw"> <el-dropdown split-button size="mini" :disabled="isDelete" style="margin-right: 10px" @click="clickDraw"> {{isDraw ? '结束绘制' : '开始绘制'}}{{drawTypeText}} <template #dropdown> <el-dropdown-menu> <el-dropdown-item @click.native="changeDrawType('plane')">飞机</el-dropdown-item> <el-dropdown-item @click.native="changeDrawType('ship')">舰船</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> <el-button :disabled="!isDraw" size="mini" @click="clearDraw">清除绘制</el-button> <el-button size="mini" :disabled="isDraw" @click="deleteDraw">{{isDelete ? '确认删除' : '删除轨迹'}}</el-button> <!-- 地图容器 --> <div id="map" class="map"></div> <!-- 地图需要展示的div内容 --> <div style="display: none"> <div id="overPlay"> <div class="overPlayText" v-if="hoverPointInfo.type == 'plane'">名称:飞燕一号</div> <div class="overPlayText" v-if="hoverPointInfo.type == 'ship'">名称:泰坦尼克号</div> <div class="overPlayText" v-if="hoverPointInfo.type == 'plane'">飞行速度:2.25马赫(2400公里/小时)</div> <div class="overPlayText" v-if="hoverPointInfo.type == 'ship'">航速:40节</div> <div class="overPlayText" v-if="hoverPointInfo.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}&lang=zh_cn&size=2&scale=1&style=8', 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方法新增overlay,overlay显示异常。换个思路,用户想要删除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项目地址