Openlayers6之地图覆盖物Overlay详解及使用,地图标注及弹窗查看详情(结合React)

demo案例:用户实现地图加载人员位置定位,并设置人员图片文字等标注,点击定位点查看人员详情。

主要通过ol/geom Point设置Styleol/Overlay实现。主要实现步骤:

  1. 实现图文标注的实质是添加点时设置Ponit的样式,图片标注就是在Style中添加Image,文字标注就是在Style中添加Text;
  2. 实现详情弹窗实质就是是通过创建Overlay,并将其添加到地图上。在用户点击到对应的点位后,设置Overlay指定的DOM元素的内容,设置position显示Overlay

overlay简述

什么是地图覆盖物Overlay?地图覆盖物主要是放置一些和地图位置相关的元素,常见的地图覆盖物有三种类型,如:popup 弹窗、label标注信息、text文本信息等,而这些覆盖物都是和html中的element等价的,通过Overlay的属性element和html元素绑定的同时设定坐标参数来达到将html元素放到地图上的位置,在平移缩放的时候html元素也会随着地图的移动而移动。Overlay的优势是可以自定义各种css样式,所以也有人使用Overlay来渲染定位

overlay API说明

Overlay初始化时可以接受很多配置参数,常用的属性有:

  • id :为对应的Overlay设置一个id,便于使用ol.MapgetOverlayById方法获取相应的overlay。
  • element:Overlay包含的DOM element,及指定Overlay使用的DOM元素。
  • offset :偏移量,像素为单位,Overlay相对于放置位置position的偏移量,默认值是 [0, 0],正值分别向右和向下偏移。
  • position:在地图所在的坐标系框架下,Overlay放置的位置。
  • positioning :根据position的位置来进行相对定位,可能的值包括bottom-leftbottom-centerbottom-rightcenter-leftcenter-centercenter-righttop-lefttop-centertop-right,默认是top-left,也就是element左上角与position重合。
  • stopEvent:地图的事件传播是否停止,默认是 true,即阻止传播,可能不太好理解,举个例子,当鼠标滚轮在地图上滚动时,会触发地图缩放事件,如果在Overlay之上滚动滚轮,并不会触发缩放事件,如果想鼠标在Overlay 之上也支持缩放,那么将该属性设置为false即可。
  • insertFirst:是否应该先添加到其所在的容器,当stopEvent设置为true时,Overlay和Openlayers的控件(controls)是放于一个容器的,此时将insertFirst设置为 true ,Overlay会首先添加到容器,这样,Overlay默认在控件的下一层(CSS z-index),所以,当stopEvent和insertFirst都采用默认值时,Overlay默认在控件的下一层。
  • autoPan :当触发Overlay setPosition方法时触发,当Overlay超出地图边界时,地图自动移动,以保证 Overlay全部可见。
  • autoPanAnimation:设置autoPan的效果动画。
  • autoPanMargin:地图自动平移时,地图边缘与Overlay的留白(空隙),单位是像素,默认是 20像素。

overlay特有方法,主要有:

  • getElement :取得包含overlay的DOM元素。隐藏Overlay时可使用Overlay.getElement.style.display = 'null'.
  • getId:取得overlay的id。
  • getMap:获取与overlay关联的map对象。
  • getOffset:获取offset属性。
  • getPosition:获取position属性.
  • getPositioning:获取positioning属性。
  • setElement:设置overlay的element。
  • setMap :设置与overlay的map对象。彻底删除Overlay使用Overlay.setMap(null)。需要注意的是删除Overlay时,Overlay创建时指定的DOM元素也一并被删除。
  • setOffset:设置offset。
  • setPosition:设置position属性。但只是将指定的DOM元素放在地图指定的位置上,DOM元素的样式和内容需要我们自己实现。
  • setPositioning:设置positioning属性。

Point图文标注实现

1. 添加点要素。

let personData = [
    {
        id: 1,
        name: '张三',
        position: [104.0641, 30.5973],
        color: 1,
        sex: 1
    }
];
// 根据positions创建一个新的数据源和要素数组,
let source = new VectorSource();
let feature = new Feature({
    geometry: new Point([104.0641, 30.5973])
});
source.addFeature(feature);
// 创建带有数据源的矢量图层
let layer = new VectorLayer({
    source: source,
    name:'pointLayer'
});
// 将矢量图层添加到Map上
map.addLayer(layer);

2. 设置feature样式。主要采用IconText

new Style({
    image: new Icon({
        scale: 0.5,
        src: '图片地址',
        anchor: [0.5, 1]
    }),
    text: new Text({
        text: '需要展示的文字', // 只能传入字符串
        fill: new Fill({
            color: '#FFFFFF'
        }),
        backgroundFill: new Fill({
            color: '#555555'
        }),
        padding: [2, 2, 0, 4],
        offsetY: -48,
        scale: 1.4
    })
});

图文标注完整代码

// 引入模块
import { Feature } from 'ol'; // 地图Collection
import { Vector as VectorLayer } from 'ol/layer';
import { Vector as VectorSource } from 'ol/source';
import { Point } from 'ol/geom';
import { Style, Icon, Fill, Text } from 'ol/style';

// 模拟数据
let personData = [
    {
        id: 1,
        name: '张三',
        position: [104.0641, 30.5973],
        color: 1,
        sex: 1
    }
];

// 设置feature样式
function setFeatureStyle(feature) {
    feature.setStyle(
        new Style({
            image: new Icon({
                scale: 0.5,
                src: '图片路径',
                anchor: [0.5, 1]
            }),
             new Text({
                text: '展示文字', // 只能传入字符串
                fill: new Fill({
                    color: '#FFFFFF'
                }),
                backgroundFill: new Fill({
                    color: '#555555'
                }),
                padding: [2, 2, 0, 4],
                offsetY: -48,
                scale: 1.4
            })
        });
    );
}
useEffect(() => {
    if (map) {
        // 如果layer已经存在,就先删除,再绘制
        if (layer) {
            layer.getSource().clear();
            map.removeLayer(layer);
            layer = null;
        }
        // 根据position创建一个新的数据源和要素数组
        let source = new VectorSource();
        for (let i = 0; i < personData.length; i++) {
            const item = personData[i];
            let feature = new Feature({
                // ol.proj.fromLonLat用于将经纬度坐标从 WGS84 坐标系转换为地图投影坐标系
                geometry: new Point(Proj.fromLonLat(item.position)),
                type: 'point'
            });
            // 设置feature样式
            setFeatureStyle(feature);
            source.addFeature(feature);
        }
        // 创建带有数据源的矢量图层
        let layer = new VectorLayer({
            source: source,
            name: 'pointLayer'
        });
        // 将矢量图层添加到Map上
        map.addLayer(layer);
    }
}, [map]);   

Overlay详情弹窗实现

上面已经讲述过详情弹窗的实质,创建的具体步骤:

1. 创建一个DOM元素,通常为div及包含的子元素。

<div className='overlayBox' id='overlayBox'>
    {/* 关闭按钮 */}
    <CloseOutlined className="close" onClick={closeOverlay} />
</div>

2. 创建一个Overlay实例并将DOM元素挂载。

// 创建一个弹窗 Overlay对象
let overlayDom = document.getElementById('overlayBox');
overlay = new Overlay({
    element: overlayDom,
    autoPan: true, 
    positioning: 'center-center', 
    offset: [0, -120],
    stopEvent: true 
});
// 将弹窗添加到Map中
map.addOverlay(overlay);

3. 监听地图点击事件。

map.on('click', (evt) => {
    // 根据点位像素位置,获取此位置的要素feature
    const feature = map.forEachFeatureAtPixel(evt.pixel, function (feature) {
        return feature;
    });
    if (feature) {}
});

4. 点击地图中的要素时读取信息并向地图Map中添加Overlay。

// 设置弹出框的位置为点击的位置 
overlay.setPosition(coordinates);

弹出窗完整代码

// 引入模块
import Overlay from 'ol/Overlay'; // 弹框

let overlay;

// 关闭弹窗
function closeOverlay() {
    overlay.setPosition(undefined);
}
useEffect(() => {
    if (map) {
        // 创建一个弹窗 Overlay对象
        let overlayDom = document.getElementById('overlayBox');
        overlay = new Overlay({
            element: overlayDom,
            autoPan: true,
            positioning: 'center-center',
            offset: [0, -120], 
            stopEvent: true
        });
        // 将弹窗添加到Map中
        map.addOverlay(overlay);

        // 监听鼠标点击
        map.on('click', (evt) => {
            // 根据点位像素位置,获取此位置的要素feature
            const feature = map.forEachFeatureAtPixel(evt.pixel, function (feature) {
                return feature;
            });
            if (feature) {
                // 获取要素的坐标
                const coordinates = feature.getGeometry().getCoordinates();
                if (overlay) {
                    // 设置弹出框的位置为点击的位置 
                    overlay.setPosition(coordinates);
                }
            } else {
                // 如果没有点击到要素,隐藏弹出框
                overlay.setPosition(undefined);
            }
        });
    }
}, [map]);

return <div className='map_container'>
    <div id='map' style={{ width: '100%', height: '100%' }}></div>
    <div className='overlayBox' id='overlayBox'>
        <CloseOutlined className="close" onClick={closeOverlay} />
    </div>
</div>;

demo实例完整代码

地图点击事件我采用了Openlayersol/interaction/Select选中实现,也可是使用监听map click实现。

import React, { useState, useEffect } from 'react';
import { CloseOutlined } from '@ant-design/icons';
import { Map, View, Feature } from 'ol'; // 地图Collection
import * as Proj from 'ol/proj'; // 转化
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'; // 图层
import { XYZ, Vector as VectorSource } from 'ol/source'; // 资源
import { Point } from 'ol/geom';
import { Style, Icon, Fill, Text } from 'ol/style'; // 样式
import Overlay from 'ol/Overlay'; // 弹框
import { Select } from 'ol/interaction';
import { click } from 'ol/events/condition';
import redImg from '@/static/image/red.png';
import blueImg from '@/static/image/blue.png';
import whiteImg from '@/static/image/white.png';
import orangeImg from '@/static/image/orange.png';
import grayImg from '@/static/image/gray.png';

let source;
let layer;
let overlay;

const OpenlayerOverlay = () => {
    const [map, setMap] = useState(null); // 地图
    const [view, setView] = useState(null); // 地图视图
    const [curInfo, setCurInfo] = useState(null); // 当前查看信息

    let personData = [
        {
            id: 1,
            name: '张三',
            position: [104.0641, 30.5973],
            color: 1,
            sex: 1
        },
        {
            id: 2,
            name: '李四',
            position: [104.0622, 30.5954],
            color: 2,
            sex: 2
        },
        {
            id: 3,
            name: '王五',
            position: [104.0722, 30.5960],
            color: 3,
            sex: 1
        },
        {
            id: 4,
            name: '张麻子',
            position: [104.0634, 30.5958],
            color: 4,
            sex: 1
        },
    ];

    // 设置feature样式
    function setFeatureStyle(feature, data) {
        if (data.color === 1) {
            feature.setStyle(
                new Style({
                    image: new Icon({
                        scale: 0.5,
                        src: redImg,
                        anchor: [0.5, 1]
                    })
                })
            );
        } else if (data.color === 2) {
            feature.setStyle(
                new Style({
                    image: new Icon({
                        scale: 0.5,
                        src: blueImg,
                        anchor: [0.5, 1]
                    })
                })
            );
        } else if (data.color === 3) {
            feature.setStyle(
                new Style({
                    image: new Icon({
                        scale: 0.5,
                        src: whiteImg,
                        anchor: [0.5, 1]
                    })
                })
            );
        } else if (data.color === 4) {
            feature.setStyle(
                new Style({
                    image: new Icon({
                        scale: 0.5,
                        src: orangeImg,
                        anchor: [0.5, 1]
                    })
                })
            );
        } else {
            feature.setStyle(
                new Style({
                    image: new Icon({
                        scale: 0.5,
                        src: grayImg,
                        anchor: [0.5, 1]
                    })
                })
            );
        }
        feature.getStyle().setText(
            new Text({
                text: data.name, // 只能传入字符串
                fill: new Fill({
                    color: '#FFFFFF'
                }),
                backgroundFill: new Fill({
                    color: '#555555'
                }),
                padding: [2, 2, 0, 4],
                offsetY: -48,
                scale: 1.4
            })
        );
    }
    // 关闭弹窗
    function closeOverlay() {
        overlay.setPosition(undefined);
    }
    useEffect(() => {
        if (map) {
            // 创建一个弹窗 Overlay对象
            let overlayDom = document.getElementById('overlayBox');
            overlay = new Overlay({
                element: overlayDom,
                autoPan: true, // 弹出窗口在边缘点击时候显示不完整,设置自动平移效果
                positioning: 'center-center', // 根据position属性的位置来进行相对定位, 默认为top-left
                offset: [0, -120], // 偏移量,像素为单位,Overlay 相对于放置位置(position)的偏移量,默认值是 [0, 0],正值分别向右和向下偏移;
                stopEvent: true // 地图的事件传播是否停止,默认是 true,即阻止传播,可能不太好理解,举个例子,当鼠标滚轮在地图上滚动时,会触发地图缩放事件,如果在 overlay 之上滚动滚轮,并不会触发缩放事件,如果想鼠标在 overlay 之上也支持缩放,那么将该属性设置为 false 即可;
            });
            // 将弹窗添加到Map中
            map.addOverlay(overlay);

            // 添加交互行为
            let selectClick = new Select({
                condition: click, // 事件类型
                style: false, // 被选中后的样式  如果不写style,将为默认样式(不是自己设置的样式,而是opelayers自带的样式),设置为false或者null将保持自己设置的样式
                // 指定图层
                filter: (feature, layer) => {
                    console.log(layer);
                    return layer;
                }
            });
            map.addInteraction(selectClick);
            // 获取当前点击元素信息,并设置弹窗显示位置
            selectClick.on("select", (e) => {
                if (e.selected.length > 0) {
                    // 获取当前点击要素信息
                    let clickPointInfo = e.selected[0].getProperties();
                    // 设置当前数据
                    setCurInfo(clickPointInfo);
                    // 设置弹出窗的位置为当前数据定位位置
                    overlay.setPosition(Proj.fromLonLat(clickPointInfo.position));
                } else {
                    setCurInfo(null);
                    overlay.setPosition(undefined);
                }
            })
        }
    }, [map]);

    useEffect(() => {
        if (map) {
            // 如果layer已经存在,就先删除,再绘制
            if (layer) {
                layer.getSource().clear();
                map.removeLayer(layer);
                layer = null;
            }
            // 根据position创建一个新的数据源和要素数组
            source = new VectorSource();
            for (let i = 0; i < personData.length; i++) {
                const item = personData[i];
                let feature = new Feature({
                    // ol.proj.fromLonLat用于将经纬度坐标从 WGS84 坐标系转换为地图投影坐标系
                    geometry: new Point(Proj.fromLonLat(item.position)),
                    id: item.id,
                    color: item.color,
                    sex: item.sex,
                    position: item.position,
                    name: item.name,
                    type: 'point'
                });
                // 设置feature样式
                setFeatureStyle(feature, item);
                source.addFeature(feature);
            }
            // 创建带有数据源的矢量图层
            layer = new VectorLayer({
                source: source,
                name: 'pointLayer'
            });
            // 将矢量图层添加到Map上
            map.addLayer(layer);
        }
    }, [map, personData]);

    // 地图鼠标样式调整
    useEffect(() => {
        if (map) {
            // 给地图绑定鼠标移动事件,检测鼠标位置所在是否存在feature来改变鼠标输入样式
            map.on('pointermove', (e) => {
                // 获取像素位置
                let pixel = map.getEventPixel(e.originalEvent);
                // 根据点位像素位置,获取此位置的要素feature
                let feature = map.forEachFeatureAtPixel(pixel, (featureOther) => {
                    return featureOther;
                });
                // 要素存在,并且是需要改变鼠标样式为pointer的feature,鼠标样式变为pointer,否则auto
                if (feature === undefined) {
                    map.getTargetElement().style.cursor = "auto";
                } else {
                    map.getTargetElement().style.cursor = "pointer";
                }
            });
        }
    }, [map]);

    useEffect(() => {
        // 监听地图视图,创建地图
        if (view) {
            // 使用高德图层
            const tileLayer = new TileLayer({
                source: new XYZ({
                    url: 'http://wprd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&scl=1&size=1&style=7&x={x}&y={y}&z={z}'
                }),
                name: 'mapLayer'
            });
            // 创建实例
            const _map = new Map({
                target: 'map',
                layers: [tileLayer], // 使用高德图层
                view: view
            });
            setMap(_map);
        }
    }, [view]);

    useEffect(() => {
        // View用于创建2D视图
        const viewObj = new View({
            // 设定中心点,因为默认坐标系为 3587,所以要将我们常用的经纬度坐标系4326 转换为 3587坐标系
            center: Proj.transform([104.06403453968424, 30.597419070782898], 'EPSG:4326', 'EPSG:3857'),
            zoom: 16
        });
        setView(viewObj);
    }, []);
    useEffect(() => {
        return () => {
            layer = null;
            source = null;
            // 删除Overlay
            // overlay.setMap(null);
            overlay = null;
            setMap(null);
            setView(null);
            setCurInfo(null);
        }
    }, []);

    return <div className='map_container'>
        <div id='map' style={{ width: '100%', height: '100%' }}></div>
        <div className='overlayBox' id='overlayBox'>
            {/* 关闭按钮 */}
            <CloseOutlined className="close" onClick={closeOverlay} />
            <div>姓名:{curInfo && curInfo.name}</div>
            <div>性别:{curInfo && curInfo.sex === 1 ? '男' : '女'}</div>
            <div>位置:{curInfo && curInfo.position[0]}, {curInfo && curInfo.position[1]}</div>
        </div>
    </div>;
}
export default OpenlayerOverlay;

采用监听Map鼠标点击事件实现demo,

// 监听鼠标点击
map.on('click', (evt) => {
    // 根据点位像素位置,获取此位置的要素feature
    const feature = map.forEachFeatureAtPixel(evt.pixel, function (feature) {
        return feature;
    });
    if (feature) {
        // 获取要素的坐标
        const coordinates = feature.getGeometry().getCoordinates();
        // 获取当前点击要素的信息
        let clickPointInfo = feature.getProperties();
        // 设置当前点击点位数据
        setCurInfo(clickPointInfo);
        if (overlay) {
            // 设置弹出框的位置为点击的位置 
            overlay.setPosition(coordinates);
        }
    } else {
        // 如果没有点击到要素,隐藏弹出框
        overlay.setPosition(undefined);
        setCurInfo(null);
    }
});
相关推荐
GIS程序媛—椰子13 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00119 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端22 分钟前
Content Security Policy (CSP)
前端·javascript·面试
木舟100926 分钟前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤439136 分钟前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安1 小时前
前端第二次作业
前端·css·css3
啦啦右一1 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习
半开半落1 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt