最近项目有用到ol来加载地图并进行一些常规的操作,ol的官网以api的格式来写的,而且全英文,对新手来说简直太不友好了(碎碎念)
所以写个文章来记录一下,以备不时之需
前言:全篇使用vue3来进行开发,但是核心代码还是js居多,引用不管什么框架都是一样的,因vue自身的响响应式特性,本文章有些函数直接更改了源数据,如果用其他框架,要注意异步问题(比如react函数式开发),小改一下就可以啦~
一、生成地图
ol加载地图有多种方式
首先需要选定一个元素作为挂载对象,注意需要给元素设置高度才能够显示地图
引入需要的类(文章举例的三种地图)
import {
TileWMS,
TileImage,
XYZ,
} from "ol/source";
import { fromLonLat, toLonLat, transform } from "ol/proj";
1.TileWMS :来自WMS服务器的图层数据源加载。
const mapref = ref(); //地图实例
//初始化地图
const InitMap = () => {
let _sourceTile = new Tile({
source: new TileWMS({
url: `${localStorage.getItem("mapUrl")}/geoserver/SeaMap/wms`,//地图在服务器上的地址
params: {//配置请求参数
LAYERS: "Groups002",//必填
TILED: true,//是否平铺
FORMAT: "image/png",//格式
},
serverType: "geoserver",//远程WMS服务器的类型:mapserver、geoserver、carmentasserver或qgis。只有当hidpi为真时才需要
crossOrigin: "anonymous",//加载图像的crosorigin属性
}),
});
mapref.value = new Map({
target: document.getElementById("modelmap"),//获取到挂在dom
layers: [_sourceTile],//图层挂载项
view: new View({
//视图
center: transform([110.25, 21.75], "EPSG:4326", "EPSG:3857"),//中心点(此处需要是地图投影)
zoom: 6,//地图初始缩放层级
}),
interactions: defaults({//地图默认事件
// mouseWheelZoom: false,
// doubleClickZoom: false,
// shiftDragZoom: false,
// dragPan: false,
}),
moveTolerance: 1, //光标必须移动的最小距离(以像素为单位)才能被检测为map move事件,而不是单击。
});
};
onMounted(() => {
InitMap();
});
2.:加载百度地图(借用TileImage类)
const InitMap = () => {
let resolutions = [];
let baiduX, baiduY;
//转换百度地图投影坐标
for (let i = 0; i < 19; i++) {
resolutions[i] = Math.pow(2, 18 - i);
}
//网格加载
let tilegrid = new TileGrid({
origin: [0, 0],
resolutions: resolutions,
});
let baidu_source = new TileImage({
projection: "EPSG:3857",//投影类型
tileGrid: tilegrid,
tileUrlFunction: function (tileCoord) {
if (!tileCoord) return "";
let z = tileCoord[0];
let x = tileCoord[1];
let y = tileCoord[2];
// 对编号xy处理
baiduX = x < 0 ? x : "M" + -x;
baiduY = -y;
return (
"http://online3.map.bdimg.com/onlinelabel/?qt=tile&x=" +
baiduX +
"&y=" +
baiduY +
"&z=" +
z +
"&styles=pl&udt=20151021&scaler=1&p=1"
);
},
});
let baidu_layer = new Tile({
source: baidu_source,
});
mapref.value = new Map({
target: "modelmap",
layers: [baidu_layer],
view: new View({
projection: "EPSG:3857",
center: [13531290.967373039, 4675318.865056401],
zoom: 12,
}),
});
};
3.XYZ: 用于具有URL模板中定义的一组XYZ格式的URL的tile数据
const InitMap = () => {
let _sourceTile = new Tile({
source: new XYZ({
url: '地图地址',
visible: true,
})
});
mapref.value = new Map({
target: document.getElementById("modelmap"),//获取到挂在dom
layers: [_sourceTile],//图层挂载项
view: new View({
//视图
center: transform([110.25, 21.75], "EPSG:4326", "EPSG:3857"),//中心点(此处需要是地图投影)
zoom: 6,//地图初始缩放层级
}),
moveTolerance: 1, //光标必须移动的最小距离(以像素为单位)才能被检测为map move事件,而不是单击。
});
};
二、地图点位加载
引入需要的函数
import Feature from "ol/Feature";
import Point from "ol/geom/Point";
import { Tile, Vector as VectorLayer, Heatmap } from "ol/layer";
import { Circle, Icon, Fill, Stroke, Style, Text } from "ol/style";
import { fromLonLat, toLonLat, transform } from "ol/proj";
数据:
const deviceList=ref([
{lng:113.15036605511219,lat:19.315288421149006,data:'第一个点'},
{lng:111.07880399646379,lat:26.00387964670233,data:'第二个点'},
])
1.点位加载
1.1通过VectorLayer加载
少量数据时,VectorLayer是最优解
//生成点位
const renderOverlay = () => {
let iconFeatureArr = deviceList.value.map((v) => {
var iconFeature = new Feature({
//feature-矢量集合图形
geometry: new Point(
transform([v.lng, v.lat], "EPSG:4326", "EPSG:3857")
),//生成集合体的形状是一个点
data: v, //原数据--将要保存的点位数据存到集合体中,在点位交互的时候可以直拿到
population: 40000,
rainfall: 500,
});
let iconStyle = new Style({//点位样式
image: new Icon({
anchor: [0.5, 0.5],
anchorXUnits: "fraction",
anchorYUnits: "pixels",
width: 40,
height: 40,
src: '点位图标地址',
}),
});
iconFeature.setStyle(iconStyle);
return iconFeature;
});
let vectorSource = new VectorSource({
features: iconFeatureArr,
});
let vectorLayer = new VectorLayer({
source: vectorSource,
});
//加入点位
mapref.value.addLayer(vectorLayer);
};
1.2 海量数据点位加载
如果使用VectorLayer加载,当叠加超过几千以上点位时就开始变慢,一万以上的要素点的时候,浏览器页面就开始卡顿或直接卡死,openlayers官方给出的优化意见是用webgl图层方式进行优化,使用webgl图层优点是渲染大量点很快,但是图标的样式不能根据要素(Feature)的风格样式(style)自定义设置,只能用图层(layer)的风格样式(style)
import WebGLPointsLayer from 'ol/layer/WebGLPoints.js';
//生成点位
const renderOverlay = () => {
let iconFeatureArr = deviceList.value.map((v) => {
var iconFeature = new Feature({
//feature-矢量集合图形
geometry: new Point(
transform([v.lng, v.lat], "EPSG:4326", "EPSG:3857")
),//生成集合体的形状是一个点
data: v, //原数据--将要保存的点位数据存到集合体中,在点位交互的时候可以直拿到
population: 40000,
rainfall: 500,
});
return iconFeature;
});
let source = new VectorSource({
features: iconFeatureArr,
wrapX: false,
});
let pointsLayer = new WebGLPointsLayer({
source: source,
style: newStyle,
});
mapref.value.addLayer(pointsLayer);
};
const newStyle = {
symbol: {
symbolType: 'circle', //图标形状可选值为:circle/triangle/square/image
size: [
//大小
'interpolate',
['linear'],
['get', 'population'],
40000,
8,
2000000,
28,
],
color: ['match', ['get', 'hover'], 1, '#ff3f3f', '#006688'], //设置hover值为1时的颜色为:#ff3f3f,默认颜色为:#006688
rotateWithView: false, //是否随视图旋转
offset: [0, 0], //偏移
opacity: [
//透明度
'interpolate',
['linear'],
['get', 'population'],
40000,
0.6,
2000000,
0.92,
],
},
};
2.点位聚合
const renderOverlay = (pointList) => {
let iconFeatureArr = pointList.map((v, index) => {
let iconFeature = new Feature({
geometry: new Point(
transform([v.lng, v.lat], "EPSG:4326", "EPSG:3857")
),
// geometry: new Point(fromLonLat(v.longitudeAndLatitudeDouble)),
data: v, //原数据
population: 40000,
rainfall: 500,
});
let iconStyle = new Style({
image: new Icon({
anchor: [0.5, 0.5],
anchorXUnits: "fraction",
anchorYUnits: "pixels",
width: 40,
height: 40,
src: '',
}),
});
iconFeature.setStyle(iconStyle);
return iconFeature;
});
// 矢量要素数据源
let source = new VectorSource({
features: iconFeatureArr,
});
// 聚合标注数据源
let clusterSource = new Cluster({
distance: 12,
source: source,
});
// 加载聚合标注的矢量图层
let styleCache = {}; //用于保存特定数量的聚合群的要素样式
let vectorLayer = new VectorLayer({
source: clusterSource,
style: function (feature, resolution) {
let size = feature.get("features").length; //获取该要素所在聚合群的要素数量
let style = styleCache[size];
if (size > 1) {
style = [
new Style({
image: new Circle({
radius: 10,
stroke: new Stroke({
color: "#fff",
}),
fill: new Fill({
color: "#3399CC",
}),
}),
text: new Text({
text: size.toString(),
fill: new Fill({
color: "#fff",
}),
}),
}),
];
} else if (size == 1) {
style = feature.get('features')[0].getStyle()
}
styleCache[size] = style;
return style;
},
});
//加入点位
mapref.value.addLayer(vectorLayer);
};
聚合后的点位非常消耗性能,如果数据量大的话,亲测会造成卡顿甚至点位消失的情况,想到一个解决方案是,在0-8层级时点位聚和,超过8层级,点位不聚和,这样可以节省一部分计算性能,但是大量点位的话,还是建议做成前后端联动的聚合
加入视图层级判断即可
三、图形绘制
1.动态图形绘制
Box:矩形;
LineString:折线
Polygon:多边形
Circle:圆形
Point:点
其中矩形需要借助圆形类来实现
先实例化一个矢量图层来作为绘画的涂鸦绘制层,再利用ol内置的Draw类来内进行绘制
import { createBox } from "ol/interaction/Draw";
import { Circle, Icon, Fill, Stroke, Style } from "ol/style";
import { Draw } from "ol/interaction";
const vector=ref()
const drawControls=ref()
//生成绘制层
const initDraw = () => {
source.value = new VectorSource({ wrapX: false });
vector.value = new VectorLayer({
//最终展示的图形样式
source: source.value,
style: new Style({
fill: new Fill({
color: "rgba(255, 255, 255, 0.2)",
}),
stroke: new Stroke({
color: "#ffcc33",
width: 10,
}),
image: new Circle({
radius: 7,
fill: new Fill({
color: "#ffcc33",
}),
}),
}),
});
//将绘制层添加到地图容器中
mapref.value.addLayer(vector.value);
};
//触发绘制函数
const drawLine = (type) => {
if (!type) return;
if (drawControls.value) mapref.value.removeInteraction(drawControls.value); //清除旧画笔
//绘制矩形
if (type === "Box") {
//绘制矩形需将value值设为Circle
drawControls.value = new Draw({
source: source.value,
type: "Circle",
geometryFunction: createBox(),
});
} else {
//勾绘类
drawControls.value = new Draw({
//source代表勾绘的要素属于的数据集
source: source.value,
type: type, //要画的geometry 类型
});
}
//双击停止
drawControls.value.on("drawend", function (e) {
const geometry = e.feature.getGeometry();
let pointArr = geometry.getCoordinates();
coordinate.value.push(pointArr);
console.log("投影坐标:", coordinate.value);
mapref.value.removeInteraction(drawControls.value); //停止绘画操作
});
mapref.value.addInteraction(drawControls.value);
};
2.测距
我们可以在地图上绘制多段直线来进行距离的测量,效果如图:
准备工作在第一小节-动态图形绘制中,绘制直线;双击停止后进行距离换算;
//双击停止
drawControls.value.on('drawend', function (e) {
const geometry = e.feature.getGeometry();
//给矢量图形添加数据
let newFeature = new Feature({
geometry: geometry,
name: type,
});
let curentLong = 0;
curentLong = geometry.getLength();//ol提供了获取距离的函数
if (curentLong >= 1000) {
curentLong = Math.round((curentLong / 1000) * 100) / 100 + ' ' + 'km';
} else {
curentLong = Math.round(curentLong) + ' ' + 'm';
}
let disstyle = new Style({
stroke: new Stroke({
color: '#ffcc33',
width: 10,
}),
text: new Text({
text: curentLong,
font: '20px sans-serif',
fill: new Fill({
color: 'red',
}),
}),
});
newFeature.setStyle(disstyle);
vector.value.getSource().addFeature(newFeature);
mapref.value.removeInteraction(drawControls.value); //停止绘画操作
});
3.面积测算
ol自带有面积测算函数
import { getArea } from 'ol/sphere'
给图形赋值的操作与测算距离一样。
let area = getArea(geometry, {
projection: 'EPSG:3857',
})
if (area > 10000) {
fontText = Math.round((area / 1000000) * 100) / 100 + ' ' + 'km²'
} else {
fontText = Math.round(area * 100) / 100 + ' ' + 'm²'
}
4.绘制带方向的线段
其实还是绘制直线,只是在绘制完成后,在终点加入了带有方向的箭头点位罢了
先在地图上添加一个展示最终结果的图层,当然,如果你想直接在绘制图层上加也是可以的,但是如果画的图形是不同样式的就不太方便了,看情况来吧
let lineStringAimSoure =ref()
lineStringAimSoure.value = new VectorLayer({
source: new VectorSource({
features: [],
}),
})
mapref.value.addLayer(lineStringAimSoure.value)
绘制直线的过程就不多说了,本章节第一小节就有,pointArr即绘制的折线点位数组,
拿到这个折线的图形数据,再给这条折线加上虚线等的样式,newFeature就是不带箭头的折线
重点来看方向箭头的加入
用折线最后两个点来计算旋转的角度,选择箭头朝向为右的图片,将点位添加到图层上即可
routearrow为箭头图片,或者你还可以画一个
let rrow = renderrowStyle(pointArr[pointArr.length - 2], pointArr[pointArr.length - 1], routearrow)
lineStringAimSoure.value.getSource().addFeatures([rrow, newFeature])
//方向箭头生成(起点,终点,箭头图片)
const renderrowStyle = (start, end, img) => {
let dx = end[0] - start[0]
let dy = end[1] - start[1]
let rotation = Math.atan2(dy, dx) //旋转角度
let arrow = new Feature({
geometry: new Point(end),
})
arrow.setStyle(
new Style({
//点位样式
image: new Icon({
src: img,
rotation: -rotation,
opacity: 0.8,
anchor: [0.5, 0.5],
width: '26',
height: '26',
rotateWithView: false,
}),
})
)
return arrow
}
或者,你想让这个线段上也显示方向,请移步第九章轨迹绘制-第二节-带方向箭头的轨迹绘制,两者结合,一个好看的带方向的折线就出现了~
四、图形回显
1.静态图形呈现
1.1 通过绘制的图形数据来加载
绘画能完成后一般会预先保存图形数据,放在数据库里保存,但是图形点位太复杂,根据坐标来呈现和复杂,我们可以直接保存图形的json数据,直接将图形加载在地图上
绘制完成时,我们可以通过writeGeometry函数拿到图形的json
需要加载的类:
import { fromLonLat, toLonLat, transform } from "ol/proj";
import GeoJSON from "ol/format/GeoJSON";
import Feature from "ol/Feature";
import { Circle, Icon, Fill, Stroke, Style, Text } from "ol/style";
import { Circle as CircleDraw } from "ol/geom.js";
拿到json操作:
//双击停止
drawControls.value.on("drawend", function (e) {
const geometry = e.feature.getGeometry();
let geoJSON = new GeoJSON().writeGeometry(geometry);
let pointArr = geometry.getCoordinates(); //圆无点数据
//将投影坐标转换为经纬度
let _arry =
type == "Point"
? toLonLat(pointArr)
: pointArr
? pointArr[0].map((item, index) => toLonLat(item))
: [];
//用以保存图形的基本数据
let currentData = {
type: type,
arry: _arry,
geoJSON: geoJSON,
};
//圆形需要获取圆心与半径,因为圆形拿不到几何json
if (type == "Circle") {
currentData.arry = [toLonLat(geometry.getCenter())];
currentData.circleRadius = geometry.getRadius();
}
//给矢量图形添加数据
let newFeature = new Feature({
geometry: geometry,
name: type,
data: currentData,
});
//将注入数据的图形加入到图层上
vector.value.getSource().addFeature(newFeature);
mapref.value.removeInteraction(drawControls.value); //停止绘画操作
});
绘制:
const drawJSONdataSource=ref()
const jsonDATA=ref([
{
"type": "LineString",
"geoJson": "{\"type\":\"LineString\",\"coordinates\":[[127632.57350790268,-652399.8016907363],[-523610.90748179913,-782648.4978886766],[-403130.9232151327,-883591.2075838663],[101582.77455188613,-883591.2075838663],[130888.76105463691,-649143.6141440019]]}"
},
{
"type": "Circle",
"center": [13288834.00506184, 5451038.795126652],
"circleRadius": 236259.58264730684,
"geoJson": "{\"type\":\"GeometryCollection\",\"geometries\":[]}"
},])
const renderDraw = (jsonDATA) => {
let geoJSON = new GeoJSON();
let FeatureArr = jsonDATA.map((item) => {
let iFeature = "";
if (item.type== "Circle") {
//圆需要根据圆心与半径单独画出来
iFeature = new Feature({
geometry: new CircleDraw(
transform(item.center, "EPSG:4326", "EPSG:3857"),
parseInt(item.circleRadius)
),
data: item,//将数据注入图形
});
} else {
//点线面
iFeature = new Feature({
geometry: geoJSON.readGeometry(item.geoJson), //直接读取图形数据
data: item,
});
}
return iFeature;
});
let vectorSource = new VectorSource({
features: FeatureArr,
});
drawJSONdataSource.value = new VectorLayer({
source: vectorSource,
name: "graphs",
});
mapref.value.addLayer(drawJSONdataSource.value);
};
1.2 通过经纬度点位信息来加载
1.2.1 圆形
圆形的加载借助Circle类
引入
import { Circle as CircleDraw } from 'ol/geom.js'
文档参数说明
一般来讲,简单的画圆只需要圆心和半径就可,如果想画包裹样式的圆才需要layout
使用:
let circleArea = new Feature({
geometry: new CircleDraw([110.64998386416187, 19.963120326685925], getRadius(2000))),
})
其中 getRadius是米向投影半径转换的函数,如下
//半径计算(米->投影)
const getRadius = (radius) => {
let metersPerUnit = mapref.value.getView().getProjection().getMetersPerUnit() //得到每单位投影的米数
let circleRadius = radius / metersPerUnit
return circleRadius
}
1.2.2 折线
通过LineString类
引入:
import LineString from 'ol/geom/LineString'
使用
coordinate 是一组折线的顶点点位
let coordinate = [
[110.64998386416187, 19.963120326685925],
[111.6206369460752, 19.5444154716524],
[111.49656850703367, 20.086461636543802],
]
let LineGeometry = new LineString([])
coordinate.forEach((item, index) => {
let currentPoint = transform(item, 'EPSG:4326', 'EPSG:3857')
LineGeometry.appendCoordinate(currentPoint)
})
let lineFeature = new Feature({ geometry: LineGeometry })
1.2.3 矩形与其他几何体
矩形与其他几何体的生成都需要借助Polygon来实现,他们有个共同点:每个几何体点位数组的第一个与最后一个必须是同一个点位,首尾相连,才能生成一个闭合的几何体
引入:
import { Polygon } from 'ol/geom'
let _Boxarry = [
[110.70575977932371, 19.630515703687436],
[111.49933780223941, 18.953036106215293],
[109.94114445797426, 18.881848596521806],
[110.27131925582971, 19.870232001209715],
[109.39664566852842, 19.374059467054124],
[110.70575977932371, 19.630515703687436],
].map((it) => transform(it, 'EPSG:4326', 'EPSG:3857'))
let boxFeature = new Feature(new Polygon([_Boxarry]))
注意,Polygon可以同时生成多个几何体,他的参数为一个数组,每个数组里的数据又都是一个几何体数组
五、点位弹窗加载
弹窗加载有两种形式,一种是手动点击点位,弹窗呈现;一种为经纬度定位到响应的点位若有点位则城下弹窗
1.通过点击加载弹窗
通过全局监听地图的点击事件,同时需要在地图的选择事件上增加addCondition事件
加载的弹窗dom:
该弹窗在初始时隐藏,通过地图调用事件来动态加载,通过detailNodeVisible控制
id="nodeModel"
v-show="detailNodeVisible"
>我是加载的弹窗
js与引用
import Overlay from 'ol/Overlay';
const detailNodeVisible = ref(false); //地图-船舶弹窗
关于Overlay:
通过地图点击事件与forEachFeatureAtPixel函数结合,通过点击处的投影坐标判断点击处是否有点位
1.1 未聚合的点位
未聚合的点位
//地图点击事件
const addPointHandle = () => {
// //添加事件
mapref.value.on('click', (evt) => {
//捕获到要素
let feature = mapref.value.forEachFeatureAtPixel(evt.pixel, (feature) => {
return feature;
});
//地图点位点击
if (feature && feature.get('data')) {
let popup = new Overlay({
element: document.getElementById('nodeModel'),//挂载的点位弹窗dom
autoPan: true,
// positioning: "center-center",//Popup放置的位置
stopEvent: true, //是否停止事件传播到地图窗口
autoPanAnimation: {
//当Popup超出地图边界时,为了Popup全部可见,地图移动的速度
duration: 250,
},
});
popup.setPosition(evt.coordinate);//此处为投影坐标
mapref.value.addOverlay(popup);
detailNodeVisible.value=true//弹窗展示
}
});
};
//被选择的图形/图标样式
let selectIconStyle = new Style({
fill: new Fill({
color: 'rgba(1, 210, 241, 0.2)',
}),
stroke: new Stroke({
color: 'red',
width: 4,
}),
image: new Icon({
anchor: [0.5, 0.5],
// anchorOrigin:'top-left',
anchorXUnits: 'fraction',
anchorYUnits: 'pixels',
// color:'red',//选中图标着色
width: 30,
height: 30,
src: chooseIcon,//chooseIcon为引入的图标地址
}),
});
// 图形选择事件
const addToolSelect = () => {
// 选择工具类
let selectTool = new Select({
multi: true,
hitTolerance: 10, // 误差
style: selectIconStyle, // 选中要素的样式
// filter: (feature, layer) => {
// console.log(feature, layer);
// return layer === drawJSONdataSource.value;
// },
// layers: [drawJSONdataSource.value],
addCondition: (evt) => {
console.log(evt.target);
},
});
//添加交互
mapref.value.addInteraction(selectTool);
};
补充:
1. 为什么不在addToolSelect函数中的addCondition上写图形选中事件
---addCondition中的响应比直接监听地图的点击事件响应要满一些,呈现的效果有一些卡顿,本质上写在哪里都可以,只是效果不同
2. 当图形与图标重叠时可以给图层增加zIndex属性,提高点位层级,这样就不会出现点位被盖住选不到的情况了,如下图:
1.2聚合后的点位
聚合后的点位在外面包裹了一层,需要用get('features')来获取点击位置的点位数组,然后通过数组中的feature拿到该点位的数据,如果点击的是聚点,那么拿到的数组长度大于1,进行放大图层操作;如果长度等于1,则说明点没有被聚合,可以加载弹窗
//地图点击事件
const addPointHandle = () => {
// //添加事件
mapref.value.on('click', (evt) => {
// console.log('click,', evt);
//捕获到要素
let feature = mapref.value.forEachFeatureAtPixel(evt.pixel, (feature) => {
return feature;
});
console.log(feature, feature.get('features'));
//地图点位点击
if (feature && feature.get('features')) {
if (feature.get('features').length == 1) {
//未聚合的点位-展开点位弹窗
let data = feature.get('features')[0].get('data'); //这样就可以拿到排在第一的点位数据
console.log(data);
let popup = new Overlay({
element: document.getElementById('nodeModel'), //挂载的点位弹窗dom
autoPan: true,
stopEvent: true, //是否停止事件传播到地图窗口
autoPanAnimation: {
duration: 250,
},
});
popup.setPosition(evt.coordinate);
mapref.value.addOverlay(popup);
detailNodeVisible.value = true; //弹窗展示
} else {
//'点击了聚合点--放大图层
mapref.value.getView().animate({
center: feature.getGeometry().getCoordinates(),
zoom: mapref.value.getView().getZoom() + 2,
});
}
}
});
};
2.通过经纬度定位到点位呈现弹窗
lng:经度
lat:纬度
//定位到某个点
const gotoPoint = (lng, lat, duration = 1000) => {
if (!lng || !lat) return;
let centers = transform([lng, lat], 'EPSG:4326', 'EPSG:3857');//转换为投影
mapref.value.getView().animate({
center: centers,
duration: duration, //动画时间
zoom: 10,
});
//经纬度锚点到船
let coords = mapref.value.getPixelFromCoordinate(centers);//从投影转为像素坐标
let feature = mapref.value.getFeaturesAtPixel(coords, {///通过页面的像素位置来定位到点位
hitTolerance: 10,//偏移量
});
console.log(feature);
//...之后的操作与上一小节点击加载相同(有未聚合的点与聚合的点的区分)
};
六、地图底图切换
数据:
// 地图层数据
const mapList = ref([
url: `/geoserver/wms`,
key: 'map',
type: 'WMS',
name: '矢量图',
img: haitu2,
visible: false,//图层是否可见
ol_uid: '',//锚点
viewTile: null, //地图图层实例
},
{
url: `/map/{z}/{x}/{y}.png`,
key: 'sea',
type: 'xyz',
name: '卫星图',
img: haitu1,
visible: true,
ol_uid: '',
viewTile: null,
},
]);
const currentMap = ref('sea'); //当前活动地图
html部分
:class="[
'changeMap',
{ normal: mapType == 'out' },
{ active: mapType == 'on' },
]"
@mouseenter="mapType = 'on'"
@mouseleave="mapType = 'out'"
>
v-for="(item, index) in mapList"
:key="index"
@click="changeMap(item, index)"
:class="['changeMapItem', { currenAfter: item.key == currentMap }]"
:data-titlle="item.name"
>
css:
.changeMap {
position: absolute;
bottom: 60px;
left: 26%;
display: flex;
transition: height 0.5s;
padding: 0 0 10px 10px;
.changeMapItem {
border: 1px solid rgba(58, 119, 159, 0);
height: 70px;
.cardBoxName {
position: absolute;
background: rgba(59, 108, 140, 0.7);
right: 1px;
bottom: 10px;
padding: 0 5px;
}
img {
height: 68px;
}
&:hover {
border: 1px solid #fff;
.cardBoxName {
background: rgb(19, 99, 174);
}
}
}
.currenAfter {
border: 1px solid rgb(25, 122, 187);
.cardBoxName {
background: rgb(19, 99, 174);
}
}
}
.normal {
.changeMapItem {
transition: all 0.8s;
margin-right: -130px;
}
}
.active {
background: rgba(58, 119, 159, 0.7);
border-radius: 5px;
padding: 10px;
.changeMapItem {
transition: all 0.8s;
margin-right: 10px;
position: relative;
&:last-child {
margin-right: 0px;
}
}
}
地图加载
同时加载两种地图,根据生成地图图层时的ol_id进行辨别;通过切换地图图层的visible属性进行完成需求
import { Tile, Vector as VectorLayer} from 'ol/layer';
import { Vector as VectorSource, TileWMS, XYZ } from 'ol/source';
import 'ol/ol.css';
import Map from 'ol/Map';
import View from 'ol/View';
//初始化地图
const InitMap = () => {
let wmsSource = mapList.value.map((item, index) => {
let _sourceTile = new Tile({
visible: item.visible,
source:
item.type == 'xyz'
? new XYZ({
url: localStorage.getItem('mapUrl') + item.url,
})
: new TileWMS({
url: localStorage.getItem('mapUrl') + item.url,
params: {
LAYERS: 'Groups002',
TILED: true,
FORMAT: 'image/png',
},
serverType: 'geoserver',
crossOrigin: 'anonymous',
}),
});
item.ol_uid = _sourceTile.ol_uid
item.viewTile = _sourceTile
return _sourceTile;
});
mapref.value = new Map({
target: document.getElementById('map'),
layers: wmsSource,
view: new View({
center: transform([110.64202144301072, 20.78474905684213], 'EPSG:4326', 'EPSG:3857'),
projection: 'EPSG:3857',
zoom: 10,
maxZoom: 16,
}),
moveTolerance: 1,
});
};
点击地图切换
//地图切换
const changeMap = (node) => {
mapList.value.forEach((item, index) => {
let isVisible = item.ol_uid == node.ol_uid
item.visible = isVisible
item.viewTile.setVisible(isVisible)
if (isVisible) {
localStorage.setItem('currentMap', item.url)
currentMap.value = item.key
}
})
}
七、地图操作
1.定位
//定位到某个点(经度,纬度,动画时间)
const gotoPoint = (lng, lat, type, duration = 1000) => {
if (!lng || !lat) return;
let centers = transform([lng, lat], 'EPSG:4326', 'EPSG:3857');
mapref.value.getView().animate({
center: centers,
duration: duration, //动画时间
zoom: 10,
});
};
2.地图缩放
//地图放大缩小(缩放大小)
const changeSize = (size) => {
mapref.value.getView().animate({
zoom: mapref.value.getView().getZoom() + size,
});
};
3.比例尺加载
位于openlayer自带control类中,可进行dom挂载,方式有多种,文章举例为在地图初始加载时就加入比例尺
官网options
引入关键函数:
import { ScaleLine, defaults } from 'ol/control';
可将比例尺通过target挂载到想要放到dom的位置,className可以改变默认样式,通过default().extend()不仅仅可以挂载比例尺,相应的地图的放大缩小图标等也可通过这个函数进行一次性加载。
关键代码:
mapref.value = new Map({
target: document.getElementById('map'),
layers: wmsSource,//上方生成的地图图层
view: new View({
center: transform(mapOpations.value.center, 'EPSG:4326', 'EPSG:3857'),
projection: 'EPSG:3857',
zoom:10,
maxZoom: 16,
}),
//比例尺
controls: new defaults().extend([
new ScaleLine({
target: document.getElementById('mapMeasuringScale'),
className: 'mapMeasuringScale',
}),
]),
moveTolerance: 1, //光标必须移动的最小距离(以像素为单位)才能被检测为map move事件,而不是单击。
});
八、热力图加载
import { Tile, Vector as VectorLayer, Heatmap } from 'ol/layer';
const thermalMapLayer=ref()//热力图图层
//加载热力图
const addThermalMap = async () => {
const list = [
{
lonAbn: 123.123,
latAbn: 123.123,
cont:2
},
{
lonAbn: 115.672337,
latAbn: 13.248612,
cont:3
},
{
lonAbn: 123.528294,
latAbn: 21.528294,
cont:1
},
{
lonAbn: 123.528294,
latAbn: 21.528294,
cont:5
},
];
thermalMapLayer.value = new Heatmap({
source: new VectorSource(),
opacity: 1, //透明度,默认1
visible: true, //是否显示,默认true
zIndex: 1, //图层渲染的Z索引,默认按图层加载顺序叠加
gradient: ['#00f', '#0ff', '#0f0', '#ff0', '#f00'], //热图的颜色渐变
blur: 15, //模糊大小(像素为单位)
radius: 8, //半径大小(像素为单位)
extent: [100, 30, 104, 40], //渲染范围,可选值,默认渲染全部
});
mapref.value.addLayer(thermalMapLayer.value);
list.forEach((item, index) => {
for (let i = 0; i < cont; i++) {
let f = new Feature({
geometry: new Point(
transform([item.lonAbn, item.latAbn], 'EPSG:4326', 'EPSG:3857')
),
});
thermalMapLayer.value.getSource().addFeature(f);
}
});
};
九、船舶轨迹绘制
1、普通轨迹绘制:
一个最普通的轨迹就是向地图上添加了一条折线,这个折线由多个拐点组成,如下
import LineString from 'ol/geom/LineString';
import Feature from 'ol/Feature';
import { Circle, Icon, Fill, Stroke, Style } from 'ol/style';
let shipTrajectory = [
{
latitude: 20.194063,
longitude: 110.249173,
time:'2023-12-12 12:00:00'
},
{
latitude: 20.1911,
longitude: 110.2322,
time:'2023-12-12 14:00:00'
},
{
latitude: 20.1886,
longitude: 110.2163,
time:'2023-12-12 15:00:00'
},
{
latitude: 20.1844,
longitude: 110.1988,
time:'2023-12-12 16:00:00'
},
];
//绘制轨迹
const createTrajectory = () => {
let LineGeometry = new LineString([]);
let coordinate = shipTrajectory;
coordinate.forEach((item, index) => {
LineGeometry.appendCoordinate(
transform([item.longitude, item.latitude], 'EPSG:4326', 'EPSG:3857')
);
});
let lineFeature = new Feature({ geometry: LineGeometry });
lineFeature.setStyle(
new Style({
fill: new Fill({
color: 'rgba(1, 210, 241, 0.2)',
}),
stroke: new Stroke({
color: 'yellow',
width: 2,
}),
})
);
let source = new VectorSource({
features: [lineFeature],
});
traskvector.value = new VectorLayer({
source: source,
});
mapref.value.addLayer(traskvector.value);
mapref.value.getView().animate({
center: transform(
[coordinate[0].longitude, coordinate[0].latitude],
'EPSG:4326',
'EPSG:3857'
),
duration: 1000,
zoom: 8,
});
};
2.带方向箭头的轨迹绘制
带方向箭头的轨迹核心是改变轨迹的样式,通过计算生成轨迹样式来绘制箭头
...
let lineFeature = new Feature({ geometry: LineGeometry });
lineFeature.setStyle(renderStyle);
...
基础绘制如上图,renderStyle函数即生成样式函数,
通过计算两个拐点之间的夹角角度,算出旋转角度,旋转箭头实现
routearrow:
//方向箭绘制
const renderStyle = (feature, resolution) => {
// 箭头样式
let styles = [];
// 线条样式
let backgroundLineStyle = new Style({
stroke: new Stroke({
color: 'green',
width: 5,
}),
});
styles.push(backgroundLineStyle);
let geometry = feature.getGeometry();
// 获取线段长度
const length = geometry.getLength();
// 箭头间隔距离(像素)
const step = 50;
// 将间隔像素距离转换成地图的真实距离
const StepLength = step * resolution;
// 得到一共需要绘制多少个 箭头
const arrowNum = Math.floor(length / StepLength);
const rotations = [];
const distances = [0];
geometry.forEachSegment(function (start, end) {
let dx = end[0] - start[0];
let dy = end[1] - start[1];
let rotation = Math.atan2(dy, dx);
distances.unshift(Math.sqrt(dx ** 2 + dy ** 2) + distances[0]);
rotations.push(rotation);
});
// 利用之前计算得到的线段矢量信息,生成对应的点样式塞入默认样式中
// 从而绘制内部箭头
for (let i = 1; i < arrowNum; ++i) {
const arrowCoord = geometry.getCoordinateAt(i / arrowNum);
const d = i * StepLength;
const grid = distances.findIndex((x) => x <= d);
styles.push(
new Style({
geometry: new Point(arrowCoord),
image: new Icon({
src: routearrow,//引入的图标样式--即箭头(也可通过canvas画一个)
opacity: 0.8,
anchor: [0.5, 0.5],
rotateWithView: false,
// 读取 rotations 中计算存放的方向信息
rotation: -rotations[distances.length - grid - 1],
scale: 0.3,
}),
})
);
}
return styles;
};
3含有节点的轨迹
有时候,轨迹不仅仅要展示方向,可能还要展示一些其他的要素。如下
当前轨迹展示了七点终点以及相应轨迹结点的时间
该分为三部分:轨迹折线、轨迹节点、轨迹时间矩形框
相应的,我们在轨迹生成的过程中一起生成上述部分
结点就是生成了点位
时间戳即生成矩形,并将文字赋予在里面
矩形通过Polygon (Polygon在第四章1.2.3有讲解。)生成,生成矩形的数组为:
_Boxarry :[[左上顶点],[右上顶点],[右下顶点],[左下顶点],[左上顶点]]
注意几何体需要首尾相接所以需要五组数据
import { LineString, Polygon } from 'ol/geom'
//绘制轨迹
const createTrajectory = () => {
let LineGeometry = new LineString([])
let coordinate = shipTrajectory
coordinate.forEach((item, index) => {
let currentPoint = transform([item.longitude, item.latitude], 'EPSG:4326', 'EPSG:3857')
LineGeometry.appendCoordinate(currentPoint)
let _Boxarry = [
currentPoint,
[currentPoint[0] + 1000, currentPoint[1]],
[currentPoint[0] + 1000, currentPoint[1] - 120],
[currentPoint[0], currentPoint[1] - 120],
currentPoint,
]
let boxFeature = new Feature(new Polygon([_Boxarry]))
boxFeature.setStyle(
new Style({
fill: new Fill({
color: 'rgba(225,225,225, 0.5)',
}),
stroke: new Stroke({
color: 'rgb(19,208,191)',
width: 2,
}),
text: new Text({
text: item.time,
font: '13px sans-serif',
fill: new Fill({
color: '#0e0e1c',
}),
}),
})
)
let pointFe = new Feature(new Point(currentPoint))
if (index == 0) {
//起点
pointFe.setStyle(
new Style({
image: new Circle({
radius: 8,
fill: new Fill({ color: 'green' }),
stroke: new Stroke({
color: 'rgba(255, 255, 255, 0.5)',
width: 4,
}),
}),
})
)
} else if (index == coordinate.length - 1) {
// 终点
pointFe.setStyle(
new Style({
image: new Circle({
radius: 8,
fill: new Fill({ color: 'red' }),
stroke: new Stroke({
color: 'rgba(255, 255, 255, 0.5)',
width: 4,
}),
}),
})
)
} else {
pointFe.setStyle(
new Style({
image: new Circle({
radius: 4,
fill: new Fill({ color: '#3399CC' }),
stroke: new Stroke({
color: 'rgba(255, 255, 255, 0.5)',
width: 2,
}),
}),
})
)
}
timePointBoxs.push(boxFeature)
timePointBoxs.push(pointFe)
})
//轨迹加入
let lineFeature = new Feature({ geometry: LineGeometry })
lineFeature.setStyle(renderStyle)
traskvector.value = new VectorLayer({
source: new VectorSource({
features: [lineFeature, ...timePointBoxs],
}),
})
mapref.value.addLayer(traskvector.value)
mapref.value.getView().animate({
center: transform(
[coordinate[0].longitude, coordinate[0].latitude],
'EPSG:4326',
'EPSG:3857'
),
duration: 1000,
zoom: 8,
});
}
4.动点轨迹运动
4.1 利用监听图层渲染实现
当只有一条轨迹时,用这个比较方便
运动轨迹
轨迹数据:
let result = [
{
latitude: 20.194063,
longitude: 110.249173,
},
{
latitude: 20.1911,
longitude: 110.2322,
},
{
latitude: 20.1886,
longitude: 110.2163,
},
{
latitude: 20.1844,
longitude: 110.1988,
},
]
首先需要绘制动点的运动轨迹
变量定义
import LineString from 'ol/geom/LineString'
import { transform, toLonLat } from 'ol/proj'
import Point from 'ol/geom/Point'
import { Circle, Icon, Fill, Stroke, Style, Text } from 'ol/style'
const traskvector = ref() //轨迹图层
const passCoordinates = ref([]) //船舶轨迹数组
const passCoordinatesCopy = ref([]) //播放暂存的轨迹
const featureLayer = ref() //动点图层
const startTime = ref(0) //动点开始动画的时间
const LineGeometry = ref() //轨迹图形
绘制轨迹函数:
此步生成了轨迹的几何图形,动点运动的一些参数(步长、经纬度)就从该几何体上获取,如果不需要显示轨迹的话,也需要该步骤,只是轨迹不必添加到视图上即可。
//绘制轨迹与动画(轨迹数组)
const createTrajectory = (coordinate) => {
traskvector.value && resetTrajectory()
if (!coordinate?.length) {
ElMessage.warning('当前时间段无历史轨迹!')
return
}
LineGeometry.value = new LineString([])
coordinate.forEach((item, index) => {
LineGeometry.value.appendCoordinate(transform(item.longitudeAndLatitudeDouble, 'EPSG:4326', 'EPSG:3857'))
})
let lineFeature = new Feature({ geometry: LineGeometry.value })
lineFeature.setStyle(
new Style({
fill: new Fill({
color: 'rgba(1, 210, 241, 0.2)',
}),
stroke: new Stroke({
color: 'yellow',
width: 2,
}),
})
)
traskvector.value = new VectorLayer({
source: new VectorSource({
features: [lineFeature],
}),
})
mapref.value.addLayer(traskvector.value) //轨迹加入
mapref.value.getView().animate({
center: transform(coordinate[0].longitudeAndLatitudeDouble, 'EPSG:4326', 'EPSG:3857'),
duration: 500,
zoom: mapref.value.getView().getZoom() + 4,
})
}
//重置轨迹
const resetTrajectory = () => {
mapref.value.un('postrender', pointMove)
mapref.value.removeLayer(traskvector.value)
mapref.value.removeLayer(featureLayer.value)
startTime.value = 0
}
实现动点运动运用了openlayers监听图层渲染事件
点击播放函数
//开始播放轨迹(点运动速度)
const startStrack = (speed=10) => {
if (featureLayer.value) {//重复播放时需要清除已有的播放事件
mapref.value.removeLayer(featureLayer.value)
passCoordinates.value = []
stopMove()
}
const particle = 20 // 轨迹分割的颗粒度,数值越小分的越细
const trackLineLen = LineGeometry.value.getLength() // 轨迹在投影平面上的长度
const resolution = mapref.value.getView().getResolution() // 当前平面的分辨率
//点有可能是小数,到终点手动添加最后一个点
const pointCount = trackLineLen / (resolution * particle)
for (let i = 0; i <= pointCount; i++) {
passCoordinates.value.push(LineGeometry.value.getCoordinateAt(i / pointCount))
}
const geoMarker = new Feature({
type: 'geoMarker',
geometry: new Point(passCoordinates.value[0]),
})
geoMarker.setId('shipPoint')
featureLayer.value = new VectorLayer({
source: new VectorSource({
features: [geoMarker],
}),
style: new Style({
image: new Circle({
radius: 8,
fill: new Fill({ color: '#333' }),
stroke: new Stroke({
color: '#f00',
width: 2,
}),
}),
}),
})
mapref.value.addLayer(featureLayer.value) //加入点位
startTime.value = new Date().getTime()
mapref.value.on('postrender', (evt) => pointMove(evt, speed))
//第一次需要手动调用一遍,否则不执行
mapref.value.render()
}
//撤销监听
const stopMove = () => {
mapref.value.un('postrender', pointMove)
}
//轨迹动画(e,播放速度,是否停止)
const pointMove = (evt, speed = 10, isStop = false) => {
if (isStop) {
stopMove()
return
} else {
// 轨迹动画的速度,数值越大位移越快
// const speed = 10
const frameState = evt.frameState
// 执行动画已经过了多少时间(秒)
const timeout = (frameState.time - startTime.value) / 1000
let count = Math.round(speed * timeout)
if (count >= passCoordinates.value.length - 1) {
// 确保到达最后一个点位,并停止移动动画
count = passCoordinates.value.length - 1
stopMove()
}
const point = featureLayer.value.getSource().getFeatureById('shipPoint')
point.getGeometry().setCoordinates(passCoordinates.value[count])
// mapref.value.getView().animate({//视野跟随
// center: passCoordinates.value[count],
// duration: speed,
// })
mapref.value.render()
}
}
4.2 多轨迹,多步骤动画
轨迹视频
利用setInternal来实现点位的移动,
轨迹数据
let data = [
{
id: 0,
nodeDtoList: [
{
id: 111,
lineList: [
[112.29555426624002, 20.636643779691937],
[112.4345752337581, 20.72870817723677],
[112.86322321693885, 20.907261520577052],
[113.36138168387862, 20.907261520577052],
[113.9116730136377, 20.907261520577052],
[114.57781514966186, 20.80448391495281],
[114.90798994751731, 20.47946420123712],
],
},
{
id: 112,
lineList: [
[112.30134680655327, 20.582462198324052],
[112.34768712905931, 20.208086935291732],
[112.57938874158943, 19.952560060085062],
[113.15285023260152, 19.707518304028724],
[113.83636998956543, 19.778346935077593],
[114.61257039154138, 19.99608338173047],
[114.83847946375828, 20.27326090227801],
[114.90219740720404, 20.36011693261669],
],
},
{
id: 113,
lineList: [
[112.35022384287798, 20.593300055732186],
[113.55346930295883, 20.468618276501886],
[114.87819627583632, 20.403526645487887],
],
},
],
},
{
id: 1,
nodeDtoList: [
{
id: 121,
lineList: [
[114.86111776443708, 20.360221634423567],
[113.89705257066203, 20.10099260434633],
[112.78412436902464, 20.180801388842298],
[112.40133377737867, 20.552700251197123],
],
},
],
},
]
变量定义:
const stepLineCoordinates = ref([]) //动画步骤-原始数据
const featureLayer = ref() //动点运动图层
const currentLines = ref([]) //动点投影轨迹数据
const currentIndex = ref(0) //当前动画进行的大步骤
const currentSpeed = ref(1) //轨迹播放速度
//动画速度数据
const speedOptions = ref([
{ value: 1, label: '×1' },
{ value: 2, label: '×2' },
{ value: 3, label: '×3' },
{ value: 4, label: '×4' },
])
dom:
.mapBox {
height: 400px;
position: relative;
.speedSelect {
position: absolute;
top: 0;
left: 20px;
z-index: 9999;
width: 70px;
}
}
用到的图片:
routearrow2:
routearrowBlue:
先画轨迹 initStepLine(步骤轨迹数组),通过addStepLine函数将轨迹的几何数据保存到源数据中,
调用播放函数runStart,开始播放轨迹动画
有点懒,回来再写解释吧。。。下方核心代码:
initStepLine(data)
//轨迹初始化
function initStepLine(stepDataList) {
if (!stepDataList.length) return
stepLineCoordinates.value = JSON.parse(JSON.stringify(stepDataList))
stepLineCoordinates.value.forEach((item, index) => {
item.nodeDtoList.forEach((it) => {
addStepLine(it.lineList, (data) => {
it.lineListGeo = data
})
})
})
}
//增加线条
function addStepLine(coordinate, fun) {
let LineGeometry = new LineString([]) //轨迹
coordinate.forEach((item, index) => {
let currentPoint = transform(item, 'EPSG:4326', 'EPSG:3857')
LineGeometry.appendCoordinate(currentPoint)
})
let lineFeature = new Feature({ geometry: LineGeometry })
lineFeature.setStyle(renderStyle)
let rrow = renderrowStyle(
transform(coordinate[coordinate.length - 2], 'EPSG:4326', 'EPSG:3857'),
transform(coordinate[coordinate.length - 1], 'EPSG:4326', 'EPSG:3857'),
routearrowBlue
)
//加入轨迹与方向箭头
finallySoure.value.getSource().addFeatures([rrow, lineFeature])
if (fun) {
//保存线条图形
fun(LineGeometry)
}
vector.value.getSource().clear()
}
//轨迹箭头生成
const renderStyle = (feature, resolution) => {
// 箭头样式
let styles = []
// 线条样式
let backgroundLineStyle = new Style({
stroke: new Stroke({
color: 'rgb(19,208,191)',
width: 2,
}),
})
styles.push(backgroundLineStyle)
let geometry = feature.getGeometry()
// 获取线段长度
const length = geometry.getLength()
// 箭头间隔距离(像素)
const step = 100
// 将间隔像素距离转换成地图的真实距离
const StepLength = step * resolution
// 得到一共需要绘制多少个 箭头
const arrowNum = Math.floor(length / StepLength)
const rotations = []
const distances = [0]
geometry.forEachSegment(function (start, end) {
let dx = end[0] - start[0]
let dy = end[1] - start[1]
let rotation = Math.atan2(dy, dx)
distances.unshift(Math.sqrt(dx ** 2 + dy ** 2) + distances[0])
rotations.push(rotation)
})
for (let i = 1; i < arrowNum; ++i) {
const arrowCoord = geometry.getCoordinateAt(i / arrowNum)
const d = i * StepLength
const grid = distances.findIndex((x) => x <= d)
styles.push(
new Style({
geometry: new Point(arrowCoord),
image: new Icon({
src: routearrow2,
opacity: 0.8,
anchor: [0.5, 0.5],
rotateWithView: false,
// 读取 rotations 中计算存放的方向信息
rotation: -rotations[distances.length - grid - 1],
scale: 0.1,
}),
})
)
}
return styles
}
//方向箭头生成(起点,终点,箭头图片)
const renderrowStyle = (start, end, img) => {
let dx = end[0] - start[0]
let dy = end[1] - start[1]
let rotation = Math.atan2(dy, dx) //旋转角度
let arrow = new Feature({
geometry: new Point(end),
})
arrow.setStyle(
new Style({
//点位样式
image: new Icon({
src: img,
rotation: -rotation,
opacity: 0.8,
anchor: [0.5, 0.5],
width: '26',
height: '26',
rotateWithView: false,
}),
})
)
return arrow
}
//动画开始播放(速度)
function runStart() {
currentIndex.value = 0 //步骤重置
//重置正在进行的动画
if (currentLines.value?.length) {
cleanintervals()
}
const particle = 5 // 轨迹分割的颗粒度,数值越小分的越细
const resolution = mapref.value.getView().getResolution() // 当前平面的分辨率
let shipList = [] //步点的投影坐标数组
stepLineCoordinates.value.forEach((item) => {
let node = {
id: item.id, //步骤id
maxLong: 0, //当前步骤最长的轨迹长度
coopList: [], //步骤内动画数组
}
item.nodeDtoList.forEach((it) => {
let _list = []
let trackLineLen = it.lineListGeo.getLength() // 轨迹在投影平面上的长度
let pointCount = trackLineLen / (resolution * particle)
for (let i = 0; i <= pointCount; i++) {
_list.push(it.lineListGeo.getCoordinateAt(i / pointCount))
}
//将最后一个点加入,形成闭环
_list.push(transform(it.lineList[it.lineList.length - 1], 'EPSG:4326', 'EPSG:3857'))
//动点
let geoMarker = new Feature({
type: 'geoMarker',
geometry: new Point(it.lineList[0]),
})
geoMarker.setId(it.id)
node.maxLong = _list.length > node.maxLong ? _list.length : node.maxLong
node.coopList.push({
pointType: 'wuuw', //点类型
array: _list, //步点的投影坐标
geoMarker: geoMarker, //动点
geoId: it.id,
})
})
shipList.push(node)
})
// console.log(shipList)
currentLines.value = shipList
//生成点位运动图层
featureLayer.value = new VectorLayer({
source: new VectorSource({
features: [],
}),
style: new Style({
image: new Circle({
radius: 8,
fill: new Fill({ color: '#333' }),
stroke: new Stroke({
color: '#f00',
width: 2,
}),
}),
}),
})
mapref.value.addLayer(featureLayer.value) //加入点位图层
//动画开始,从第一步骤开始运行
renderStep(currentLines.value[0], currentSpeed.value)
}
//清除定时器
const cleanintervals = () => {
featureLayer.value && mapref.value.removeLayer(featureLayer.value)
currentLines.value.forEach((item) => {
item.coopList.forEach((it) => {
it.inter && clearInterval(it.inter)
})
})
}
//步骤播放(步骤数据,运动速度-->数据越大,运动越快)
const renderStep = (node, speed = 1) => {
node.coopList.forEach((it) => {
featureLayer.value.getSource().addFeature(it.geoMarker) //加入动点
let point = featureLayer.value.getSource().getFeatureById(it.geoId) //获取动点源
it.long = it.long ? it.long : 0 //记录轨迹播放步数
renderInternal(it, point, speed, node.maxLong)
})
}
//生成定时器动画(单个轨迹数据,动点源,运动速度,当前步骤最大长度)
const renderInternal = (it, point, speed, maxLong) => {
it.inter = setInterval(() => {
it.long += speed
//防止步长太大超过数组长度
if (it.long > it.array.length - 1) {
it.long = it.array.length - 1
}
point.getGeometry().setCoordinates(it.array[it.long]) //动点运动
//单轨迹结束
if (it.long == it.array.length - 1) {
clearInterval(it.inter)
point && featureLayer.value.getSource().removeFeature(point) //清点
//轨迹最长时,大步骤结束
if (maxLong == it.array.length) {
currentIndex.value++ //步骤加一
if (currentIndex.value < currentLines.value.length) {
//大步骤结束
renderStep(currentLines.value[currentIndex.value], speed)
} else {
//全部结束
currentIndex.value = 0
}
}
}
}, 100)
}
//改变播放速度
const changeSteep = (speed) => {
console.log(speed)
currentLines.value[currentIndex.value].coopList.forEach((item) => {
clearInterval(item.inter)
featureLayer.value.getSource().clear()
})
currentSpeed.value = speed
renderStep(currentLines.value[currentIndex.value], speed)
}
参考链接
发表评论