实现类似于电子围栏的功能,在限制区域内绘制和编辑多边形,不允许多边形任何一个边与限制区域边界相交超过一个点。
先将限制边界转为turf下的linestring和polygon要素。简称TL要素和TP要素。
主要实现思路是通过openlayers的Draw方法控制绘制点不超出限制边界,再通过地图单击事件拿到鼠标点下后的点,将该点转为turf的point点要素,通过turf的booleanPointInPolygon方法判断该点是否在TP要素内,如果在TP要素内并且该点与上一次点击的点不是同一个点就记录下该点,存储到数组中 。通过当前点击的点与上一个点生成连线,通过turf将两点生成的连线转为truf的linestring要素,再将限制边界也转为truf下的linestring要素,通过truf的lineIntersect判断两条linestring要素是否相交,如果相交点超过两个,通过removeLastPoint方法删除最后绘制的点并且将其移出数组,以此达到多边形不超出限制区域的目的。同理,地图双击事件即是将鼠标点击下的点与上一个点和绘制的第一个点生成连线,判断相交点不超过两个,达到目的
代码部分
<template>
<div id="map" class="_map" v-loading="loading" element-loading-text="加载中..."></div>
</template>
<script setup>
import 'ol/ol.css';
import { Map as olMap, View } from 'ol';
import { Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';
import { Vector as VectorSource, WMTS } from 'ol/source';
import WMTSTileGrid from 'ol/tilegrid/WMTS';
import { getTopLeft } from 'ol/extent';
import { defaults as defaultControls } from 'ol/control';
import Modify from 'ol/interaction/Modify';
import Draw from 'ol/interaction/Draw'
import {Fill, Stroke, Style, Text, Circle, Icon} from 'ol/style';
import * as turf from '@turf/turf'
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const map = ref(); // 地图
const limitationLayer = ref(); // 固定限制区域图层
const drawLayer = ref(); // 图形绘制后的显示图层
const linestring = ref(); // turf的linestring图层
const polygon = ref(); // turf的polygon图层
const draws = ref(); // 绘制图形变量
const modifys = ref(); // 编辑图形变量
const drawArr = ref([]); //
const controlBool = ref(true) // 控制仅允许绘制一个图形
/** 初始化地图 */
const initMap = () => {
map.value = new olMap({
target: 'map',
layers: [
addOpenMapTiandituLayer('img', '5f1d316698***********', 1, false), // 换成自己的天地图秘钥
addOpenMapTiandituLayer('ibo', '5f1d316698***********', 2, false),
addOpenMapTiandituLayer('cia', '5f1d316698***********', 3, false),
],
view: new View({ // 地图视图
projection: gridName, // 坐标系,有EPSG:4326和EPSG:3857
center: props.center, // 中心点坐标
minZoom: props.zoomMin || 4, // 地图缩放最小级别
extent:projectionExtent, // 区域限制
zoom: props.defaultZoom // 地图缩放级别(打开页面时默认级别)
}),
controls: defaultControls({
zoom: false,
rotate: false,
attribution: false
})
})
/** 添加固定限制区域 */
limitationLayer.value = addJsonVectorLayer('' ,9)
/** 固定限制区域样式 */
limitationLayer.value.setStyle(createStyle({stroke: {width: 1, color: 'red'}, fill: {color: 'rgba(0,0,0,0)'}}))
map.value.getView().fit(limitationLayer.value.getSource().getExtent() ,{ padding: [150, 300, 150, 300] })
/** 转换为turf的lineString图层 */
linestring.value = turf.lineString(limitationLayer.value.getSource().getFeatures()[0].getGeometry().getCoordinates()[0][0])
/** 转换为turf的polygon图层 */
polygon.value = turf.polygon(limitationLayer.value.getSource().getFeatures()[0].getGeometry().getCoordinates()[0])
/** 添加绘制图形图层 */
drawLayer.value = addEmptyVectorLayer(10)
/** 绘制完成后的图形样式 */
drawLayer.value.setStyle(createStyle({stroke: {color: '#FF8139', width: 3}, fill: {color: 'rgba(0,237,45,0)'}}))
/** 添加图形绘制交互 */
draws.value = addDrawFeature(drawLayer.value, '', 'Polygon')
/** 添加编辑交互 */
modifys.value = modifyFeature(drawLayer.value)
/** 地图单击事件,用于在限制区域内打点,判断当前点和上一个点与当前点的连线是否超出限制 */
map.value.on('click', mapClick());
/** 地图双击事件,双击结束绘制 */
map.value.on('dblclick', mapDoubleClick())
/** 监听开始绘制事件 */
draws.value.addEventListener('drawstart',(evt) => {
evtList.value = evt
drawArr.value = []
modifys.value.setActive(false)
})
/** 监听结束绘制事件 */
draws.value.addEventListener('drawend',(evt) => {
draws.value.setActive(false)
modifys.value.setActive(true)
drawArr.value = []
controlBool.value = false
})
let prevControl = null // 记录上一步编辑成功的图层状态
/** 监听开始编辑事件 */
modifys.value.on('modifystart', (e) => {
prevControl = drawLayer.value.getSource().getFeatures()[0].clone()
})
/** 监听结束编辑事件 */
modifys.value.on('modifyend', (e) => {
let arrs = drawLayer.value.getSource().getFeatures()[0].getGeometry().getCoordinates()[0]
let indexArr = []
arrs.forEach((item, index) => {
if(JSON.stringify(item) == JSON.stringify(e.mapBrowserEvent.coordinate)) {
indexArr.push(index)
}
})
if(indexArr.length == 0) {
return false
}
let point = turf.point(arrs[indexArr[0]])
let isPointInPolygon = turf.booleanPointInPolygon(point, polygon.value);
if(!isPointInPolygon) {
proxy?.$modal.msgWarning('当前编辑的图形超出限制区域,请重新编辑!')
drawLayer.value.getSource().getFeatures()[0].setGeometry(prevControl.getGeometry())
} else if(indexArr.length > 1) {
let line1 = turf.lineString([arrs[0], arrs[1]])
let line2 = turf.lineString([arrs[0], arrs[arrs.length - 2]])
let intersects1 = turf.lineIntersect(line1, linestring.value);
let intersects2 = turf.lineIntersect(line2, linestring.value);
if(intersects1.features.length > 1 || intersects2.features.length > 1) {
proxy?.$modal.msgWarning('当前编辑的图形超出限制区域,请重新编辑!')
drawLayer.value.getSource().getFeatures()[0].setGeometry(prevControl.getGeometry())
}
} else if(indexArr.length == 1) {
let line1 = turf.lineString([arrs[indexArr[0]], arrs[indexArr[0] - 1]])
let line2 = turf.lineString([arrs[indexArr[0]], arrs[indexArr[0] + 1]])
let intersects1 = turf.lineIntersect(line1, linestring.value);
let intersects2 = turf.lineIntersect(line2, linestring.value);
if(intersects1.features.length > 1 || intersects2.features.length > 1) {
proxy?.$modal.msgWarning('当前编辑的图形超出限制区域,请重新编辑!')
drawLayer.value.getSource().getFeatures()[0].setGeometry(prevControl.getGeometry())
}
}
})
// draws.value.setActive(false) //TODO 关闭绘制功能,可在初始化时关闭,自己控制是否绘制
// modifys.value.setActive(false) //TODO 关闭编辑功能,可在初始化时关闭,自己控制是否编辑
}
/** 地图单击事件 */
function mapClick() {
return function(event) {
// 获取点击的坐标
if(controlBool.value) {
let coordinates = event.coordinate;
let point = turf.point(coordinates)
let isPointInPolygon = turf.booleanPointInPolygon(point, polygon.value);
if(drawArr.value) {
if(isPointInPolygon && JSON.stringify(drawArr.value[drawArr.value.length - 1]) != JSON.stringify(coordinates)) {
drawArr.value.push(coordinates)
}
if(drawArr.value.length > 2) {
drawArr.value.splice(0, 1)
}
if(drawArr.value.length == 2) {
let line = turf.lineString(drawArr.value)
let intersects = turf.lineIntersect(line, linestring.value);
if(intersects.features.length > 1) {
proxy?.$modal.msgWarning('绘制图形超出限制区域,请重新绘制!')
drawArr.value.splice(drawArr.value.length - 1, 1)
draws.value.removeLastPoint()
}
}
}
}
}
}
/** 地图双击事件 */
function mapDoubleClick() {
return function(event) {
let coordinates = event.coordinate;
if(evtList.value) {
let line = turf.lineString([coordinates, evtList.value.feature.getGeometry().getCoordinates()[0][0]])
let intersects = turf.lineIntersect(line, linestring.value);
let arrs = evtList.value.feature.getGeometry().getCoordinates()[0]
arrs.forEach((item, index, arr) => {
arr[index] = item.join(',')
})
if(intersects.features.length > 1) {
proxy?.$modal.msgWarning('绘制图形超出限制区域,请重新绘制!')
drawArr.value.splice(drawArr.value.length - 1, 1)
draws.value.removeLastPoint()
} else if(Array.from(new Set(arrs)).length > 2){
draws.value.finishDrawing()
drawArr.value = []
}
}
}
}
/** 底图加载 */
function addOpenMapTiandituLayer(serviceName, tk, zIndex, addBool = true){
/**
* 添加天地图
* serviceName 选项如下,更多图层查阅天地图官方网站
* img: 影像底图
* vec: 矢量底图
* cva : 矢量注记
* ter: 地形晕渲
* tk:秘钥
*/
const layerObj = new TileLayer({
'zIndex':zIndex,
'source': new WMTS({
'url': "https://t{0-7}.tianditu.gov.cn/" + serviceName +"_c/wmts?tk="+tk,
'crossOrigin':'anonymous',
'tileGrid': new WMTSTileGrid({
'origin': getTopLeft(projectionExtent),
'resolutions':resolutions,
'matrixIds':ids
}),
'format': 'tiles',
'layer': serviceName,
'matrixSet': 'c',
'style': ''
})
});
if(addBool){
map.value.addLayer(layerObj)
}
return layerObj
}
/** 创建空矢量图层 */
function addEmptyVectorLayer(zIndex, bool=true){
const layerObj = new VectorLayer({
'zIndex':zIndex || 1,
'source': new VectorSource({
'format': jsonFormat
})
});
if(bool) {
map.value.addLayer(layerObj);
}
return layerObj;
}
/** 添加本地json图层 */
function addJsonVectorLayer (jsonData,zIndex, bool=true) {
const layerObj = new VectorLayer({
'zIndex':zIndex || 1,
'source': new VectorSource({
'features': jsonFormat.readFeatures(jsonData)
})
});
if(bool) {
map.value.addLayer(layerObj);
}
return layerObj;
}
/***********
* 绘制点、线、面
* @param layer 绘制图形图层,必须为矢量图层
* @param styles 绘制过程中的图形颜色
* @param drawType 绘制类型 'Point' 'LineString' 'Polygon' 'Circle'
*/
function addDrawFeature(layer, styles, drawType= 'Point'){
let drawStyle = styles ? styles : createStyle({ stroke:{color: '#FF8139', width: 3}, fill:{color: 'rgba(255, 255, 255, 0.3)'}, image: {radius: 5, stroke: {width: 1, color: '#FFFFFF'}, fill: {color: '#FF8139'}} })
let draw = new Draw({
source: layer.getSource(),
type: drawType,
style: drawStyle,
condition: function (e) {
let features = map.value.getFeaturesAtPixel(e.pixel, { layerFilter: function (layer) { return layer === limitationLayer.value; } });
if (features != null && features.length > 0) {
return true;
} else {
proxy?.$modal.msgWarning('绘制图形超出限制区域,请重新绘制!')
return false;
}
},
finishCondition:(e)=>{return false}
})
map.value.addInteraction(draw)
return draw
}
/********
* 选中图形并编辑
*/
function modifyFeature(layer){
const modify = new Modify({
source: layer.getSource()
});
map.value.addInteraction(modify)
return modify;
}
/** 创建图层样式 */
function createStyle(styleConfig){
/**
* 根据 json 格式,创建样式,并返回 Style 对象
*/
styleConfig = styleConfig || {};
// 创建图层样式
const styleObj = new Style();
// 边框
const stroke = styleConfig['stroke'] || {};
const strokeStyle = new Stroke({
'color': stroke['color'] || 'rgba(0,0,255,1.0)',
'width': stroke['width'] || 2,
'lineDash': stroke['lineDash'] || ''
});
styleObj.setStroke(strokeStyle);
// 填充
const fill = styleConfig['fill'] || {};
const fillStyle = new Fill({
'color': fill['color'] || 'rgba(255,0,0,0.5)',
});
styleObj.setFill(fillStyle);
// 文字
const text = styleConfig['text'] || {};
const textStyle = new Text({
'text': text['text'] || '',
'padding': text['padding'] || [0,0,0,0],
'placement': text['placement'] || 'point',
'overflow': text['overflow'] || false,
'font': text['font'] || 'normal 14px 微软雅黑',
'offsetX': text['offsetX'] || 0,
'offsetY': text['offsetY'] || 0
});
if (!!text['fill']){
const fill = new Fill({
'color':text.fill['color']
});
textStyle.setFill(fill);
}
if (!!text['backgroundFill']){
const backgroundFill = new Fill({
'color':text.backgroundFill['color']
});
textStyle.setBackgroundFill(backgroundFill);
}
if (!!text['stroke']){
const stroke = new Stroke({
'color':text.stroke['color']
});
textStyle.setStroke(stroke);
}
styleObj.setText(textStyle);
// 图片
const icon = styleConfig['icon'];
if (icon){
let obj:any = {}
if(icon['anchor']) {
obj['anchor'] = icon['anchor']
}
if(icon['anchorOrigin']) {
obj['anchorOrigin'] = icon['anchorOrigin']
}
if(icon['displacement']) {
obj['displacement'] = icon['displacement']
}
if(icon['offset']) {
obj['offset'] = icon['offset']
}
if(icon['src']) {
obj['src'] = icon['src']
}
if(icon['img']) {
obj['img'] = icon['img']
}
if(icon['imgSize']) {
obj['imgSize'] = icon['imgSize']
}
if(icon['size']) {
obj['size'] = icon['size']
}
const iconStyle = new Icon(obj);
styleObj.setImage(iconStyle);
}
const image = styleConfig['image'];
if(image){
const imageStyle = new Circle();
if(image['radius']){
imageStyle.setRadius(image['radius'])
}
if(image['stroke']){
const stroke = new Stroke({
'color': image.stroke['color'],
'width': image.stroke['width'] || 2
});
imageStyle.setStroke(stroke)
}
if(image['fill']){
const fill = new Fill({
'color': image.fill['color']
});
imageStyle.setFill(fill)
}
styleObj.setImage(imageStyle);
}
return styleObj;
}
onMounted(() => {
initMap()
})
</script>
<style lang="scss" scoped>
._map {
width: 100%;
height: 100%;
position: relative;
}
</style>