demo案例:用户实现地图加载人员位置定位,并设置人员图片文字等标注,点击定位点查看人员详情。
主要通过ol/geom Point
设置Style
和ol/Overlay
实现。主要实现步骤:
- 实现图文标注的实质是添加点时设置
Ponit
的样式,图片标注就是在Style
中添加Image
,文字标注就是在Style
中添加Text
; - 实现详情弹窗实质就是是通过创建
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.Map
的getOverlayById
方法获取相应的overlay。 - element:Overlay包含的DOM element,及指定Overlay使用的DOM元素。
- offset:偏移量,像素为单位,Overlay相对于放置位置
position
的偏移量,默认值是 [0, 0],正值分别向右和向下偏移。 - position:在地图所在的坐标系框架下,Overlay放置的位置。
- positioning:根据
position
的位置来进行相对定位,可能的值包括bottom-left
、bottom-center
、bottom-right
、center-left
、center-center
、center-right
、top-left
、top-center
、top-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样式。主要采用Icon
和Text
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);
}
});