3 OpenLayers学习笔记-渲染点位到地图

313 阅读8分钟

需求

从后台请求回大量的经纬度点位数据,前端将点位渲染到地图上。鼠标悬浮到点位上时,鼠标指针变成pointer的状态,点击点位获取到点位ID,进行后续操作。

实现过程

  • 基于Feature实现点位渲染,添加的feature使用了两种方式实现。

    1. 根据经纬度,通过调用new ol.Feature()创建feature组成创建资源,再创建图层。

    2. 根据经纬度,将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.Featuregeometry参数需要使用投影坐标系。

  • map.getEventPixel(e.originalEvent)在鼠标事件中调用,获取当前鼠标的像素位置,等价于js原生鼠标事件对象的[clientX, clientY]e.originalEvent是原生事件对象。

  • map.forEachFeatureAtPixel函数用于在指定像素坐标处查找feature并调用回调函数,以执行特定的操作。

  • feature.getGeometry函数用于获取要素(feature)的几何对象(geometry object)。

  • feature.getGeometry().getType()函数用于获取要素(feature)的几何类型。返回的是一个字符串,表示要素的几何类型,例如 PointLineStringPolygon 等。

  • 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集合的typeFeatureCollection

  • 自定义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官方文档

layer.getSource().forEachFeature官方文档

自定义GeoJSON官方栗子

ol.geom.Point官方文档