最近项目有用到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"

>我是加载的弹窗