需求
从后台请求回大量的经纬度点位数据,前端将点位渲染到地图上。鼠标悬浮到点位上时,鼠标指针变成pointer的状态,点击点位获取到点位ID,进行后续操作。
实现过程
-
基于
Feature实现点位渲染,添加的feature使用了两种方式实现。-
根据经纬度,通过调用
new ol.Feature()创建feature组成创建资源,再创建图层。 -
根据经纬度,将
feature放到GeoJSON中,关联GeoJSON创建资源,再创建图层。
- 第一种方式在创建
feature的同时,设置feature的样式。第二种方式在GeoJSON中统一定义feature,在创建资源后(new ol.source),遍历资源中的feature设置样式。
-
-
通过监听
pointermove事件,结合map.forEachFeatureAtPixel函数,实现鼠标悬浮在feature上时改变鼠标状态的功能。 -
通过监听
click事件,结合map.forEachFeatureAtPixel函数,实现点击feature的功能。
知识点
-
ol.proj.fromLonLat([lon, lat])用于将经纬度从WGS84坐标系转换为地图投影坐标系,new ol.Feature的geometry参数需要使用投影坐标系。 -
map.getEventPixel(e.originalEvent)在鼠标事件中调用,获取当前鼠标的像素位置,等价于js原生鼠标事件对象的[clientX, clientY]。e.originalEvent是原生事件对象。 -
map.forEachFeatureAtPixel函数用于在指定像素坐标处查找feature并调用回调函数,以执行特定的操作。 -
feature.getGeometry函数用于获取要素(feature)的几何对象(geometry object)。 -
feature.getGeometry().getType()函数用于获取要素(feature)的几何类型。返回的是一个字符串,表示要素的几何类型,例如Point、LineString、Polygon等。 -
map.getTargetElement()用于获取new ol.Map()时传入的target参数对应的DOM元素。 -
new ol.style.Icon的参数src可以设置为图片的路径,也可以设置为图片的base64字符串。 -
new ol.Feature时,可以传入自定义的参数,并通过feature.get('参数名')获取。
const feature = new ol.Feature({
geometry: new ol.geom.Point(ol.proj.fromLonLat(e)),
custom: {
id: Math.ceil(Math.random() * 100000)
},
fromLayerID: 'addFeaturesLayer'
});
// custom和fromLayerID是自定义的参数,可通过feature.get('custom')和feature.get('fromLayerID')获得
-
map.getLayers()函数用于获取地图上的所有图层(layers),map.getLayers().getArray()方法获取图层数组。图层也有get方法,用于获取图层上的属性,自带的属性和自定义的属性都能获取到。 -
layer.getSource().removeFeature(feature)用于将feature从图层上删除。 -
自定义
GeoJSON时,feature集合的type是FeatureCollection。 -
自定义
GeoJSON时,自定义属性放在properties下,可通过feature.get('参数名')获取。 -
layer.getSource().forEachFeature((f) => {})函数用于遍历图层上所有的feature。 -
map.getView().calculateExtent(map.getSize())获取屏幕进度范围,返回结果为[minLon, minLat, maxLon, maxLat]。
代码HTML+CSS+JS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/8.2.0/ol.min.css" integrity="sha512-bc9nJM5uKHN+wK7rtqMnzlGicwJBWR11SIDFJlYBe5fVOwjHGtXX8KMyYZ4sMgSL0CoUjo4GYgIBucOtqX/RUQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<title>点位渲染</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#app,
.app-map {
height: 100%;
}
.app-btns {
position: fixed;
right: 10px;
top: 10px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
width: 210px;
padding: 25px;
text-align: center;
border-radius: 5px;
display: flex;
flex-direction: column;
}
.app-btns button {
font-size: 18px;
border: none;
padding: 12px 20px;
border-radius: 4px;
color: #fff;
background-color: #409eff;
border-color: #409eff;
cursor: pointer;
border: 1px solid #dcdfe6;
margin-bottom: 5px;
}
.app-btns button:hover {
background-color: rgba(7, 193, 96, 0.8);
}
.app-btns button.active {
background-color: #07c160;
}
</style>
</head>
<body>
<div id="app">
<div class="app-map" id="app-map"></div>
<div class="app-btns">
<button type='button' :class='{active: type === 1}' @click='handleClickAddFeatures'>加载方式1</button>
<button type='button' :class='{active: type === 2}' @click='handleClickGeoJSON'>加载方式2</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/8.2.0/dist/ol.min.js" integrity="sha512-+nvfloZUX7awRy1yslYBsicmHKh/qFW5w79+AiGiNcbewg0nBy7AS4G3+aK/Rm+eGPOKlO3tLuVphMxFXeKeOQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.14/vue.global.prod.min.js" integrity="sha512-huEQFMCpBzGkSDSPVAeQFMfvWuQJWs09DslYxQ1xHeaCGQlBiky9KKZuXX7zfb0ytmgvfpTIKKAmlCZT94TAlQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
const { createApp } = Vue;
// feature图片
const base64Img = 'data:image/png;base64,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';
// feature样式
const monitorIcon = new ol.style.Style({
image: new ol.style.Icon({
src: base64Img,
scale: 1,
anchor: [0.5, 0.5],
rotateWithView: true,
rotation: 0,
opacity: 1
})
});
createApp({
data() {
return {
type: 0, // 加载方式
map: {}, // 地图实例
currentMonitorLayer: null // 当前监控图层
}
},
methods: {
// 初始化地图
initMap() {
// 高德地图瓦片地址
const vectorLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'http://wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}'
}),
name: '初始化地图图层'
});
// 初始化地图
this.map = new ol.Map({
target: 'app-map',
layers: [vectorLayer],
view: new ol.View({
projection: 'EPSG:3857',
//设定中心点,因为默认坐标系为 3587,所以要将我们常用的经纬度坐标系4326 转换为 3587坐标系
center: ol.proj.transform([105.4315021624999, 38.25918891134296], 'EPSG:4326', 'EPSG:3857'),
zoom: 5,
})
});
// 绑定地图事件
this.bindMapEvt();
},
// 绑定地图事件
bindMapEvt() {
const { map } = this;
// 监听鼠标移动,移动到feature上时,鼠标变为可点击的状态
map.on('pointermove', (e) => {
let pixel = map.getEventPixel(e.originalEvent);
let feature = map.forEachFeatureAtPixel(pixel, (feature) => {
return feature
})
if (feature == undefined || feature.getGeometry().getType() != 'Point') {
map.getTargetElement().style.cursor = 'auto'
} else {
map.getTargetElement().style.cursor = 'pointer'
}
});
// 监听鼠标点击
map.on('click', (evt) => {
let clickPoint = ol.proj.transform(evt.coordinate, 'EPSG:3857', 'EPSG:4326')
console.log('当前点击坐标为 : ' + clickPoint[0].toFixed(7) + ',' + clickPoint[1].toFixed(7));
// 根据像素位置获取所有图层上的要素(features)
const feature = map.forEachFeatureAtPixel(evt.pixel, function(feature) {
return feature;
});
if (feature) {
// 获取自定义参数
const customValue = feature.get('custom');
// 获取feature上增加的fromLayerID字段
const featureLayerID = feature.get('fromLayerID');
// 获取所有图层
const layers = map.getLayers().getArray();
if (featureLayerID === 'addFeaturesLayer') {
alert(`点击的feature的自定义参数是${customValue.id},这个是通过addFeatures方法添加的feature,点击会将其删除。`);
// 根据layerID获取feature所在的图层
const addFeaturesLayer = layers.find(e => e.get('layerID') === featureLayerID);
addFeaturesLayer.getSource().removeFeature(feature);
};
if (featureLayerID === 'geoJSONLayer') {
alert(`点击的feature的自定义参数是${customValue.id},这个是通过GeoJSON方法添加的feature,点击会将其删除。`);
// 根据layerID获取feature所在的图层
const geoJSONFeaturesLayer = layers.find(e => e.get('layerID') === featureLayerID);
geoJSONFeaturesLayer.getSource().removeFeature(feature);
};
}
})
},
// new ol.Feature()的方式定义feature
handleClickAddFeatures() {
this.type = 1;
// 删除上一次的图层
this.currentMonitorLayer && this.map.removeLayer(this.currentMonitorLayer);
const { map } = this;
const positions = this.createCircularPosition2(); // 生成数据
console.time('new ol.Feature方式');
// 根据positions创建一个新的数据源和要素数组,
const vectorSource = new ol.source.Vector({
features: positions.map(e => {
// ol.proj.fromLonLat用于将经纬度坐标从 WGS84 坐标系转换为地图投影坐标系
const feature = new ol.Feature({
geometry: new ol.geom.Point(e),
custom: {
id: Math.ceil(Math.random() * 100000)
},
fromLayerID: 'addFeaturesLayer'
});
feature.setStyle(monitorIcon);
return feature;
})
});
// 创建带有数据源的矢量图层
const featuresLayer = new ol.layer.Vector({
source: vectorSource,
layerID: 'addFeaturesLayer',
name: '点位图层1'
});
// 将矢量图层添加到地图上
map.addLayer(featuresLayer);
this.currentMonitorLayer = featuresLayer;
console.timeEnd('new ol.Feature方式');
},
// 将feature添加到GeoJSON数据内,再加到地图上
handleClickGeoJSON() {
this.type = 2;
// 删除上一次的图层
this.currentMonitorLayer && this.map.removeLayer(this.currentMonitorLayer);
const { map } = this;
const positions = this.createCircularPosition(); // 生成数据
// 创建JSON
const monitorGeoJSON = {
"type": "FeatureCollection",
"features": []
};
console.time('GeoJSON方式');
// 根据positions中的坐标数据,添加到JSON的features数组里面
for (let i = 0; i < positions.length; i++) {
monitorGeoJSON.features.push({
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": positions[i]
},
"properties": {
"name": "监控点",
"description": "这是一个监控点位",
"fromLayerID": "geoJSONLayer",
"custom": {
id: Math.ceil(Math.random() * 99999)
}
}
})
};
// 读取JSON数组,创建矢量图层
const geoJSONLayer = new ol.layer.Vector({
source: new ol.source.Vector({
features: new ol.format.GeoJSON().readFeatures(monitorGeoJSON, { featureProjection: 'EPSG:3857' })
}),
layerID: 'geoJSONLayer',
name: '点位图层2'
});
// 给feature设置样式
geoJSONLayer.getSource().forEachFeature(function(feature) {
feature.setStyle(monitorIcon);
});
// 将矢量图层添加到地图上
map.addLayer(geoJSONLayer);
this.currentMonitorLayer = geoJSONLayer;
console.timeEnd('GeoJSON方式');
},
// 创建圆形范围数据
createCircularPosition() {
const center = [108.55, 34.32];
const circularManyPosition = [];
for (let i = 0; i < 1000; i++) {
// 生成随机半径在-20到20之间
const radius = Math.random() * 40 - 20;
// 生成随机角度
const angle = Math.random() * 2 * Math.PI;
// 计算新点的坐标
const newX = center[0] + radius * Math.cos(angle);
const newY = center[1] + radius * Math.sin(angle);
// 将新点添加到数组
circularManyPosition.push([newX, newY]);
}
return circularManyPosition;
},
// 创建数据矩形范围数据
createCircularPosition2 () {
const [minLon, minLat, maxLon, maxLat] = this.map.getView().calculateExtent(this.map.getSize());
const circularManyPosition = [];
for (let i = 0; i < 1000; i++) {
const lon = minLon + Math.random() * (maxLon - minLon);
const lat = minLat + Math.random() * (maxLat - minLat);
// 将新点添加到数组
circularManyPosition.push([lon, lat]);
}
return circularManyPosition;
}
},
mounted() {
this.initMap();
}
}).mount('#app')
</script>
</body>
</html>
参考文章
ol.format.GeoJSON().readFeatures官方文档