實現(xiàn)類似于電子圍欄的功能僵闯,在限制區(qū)域內(nèi)繪制和編輯多邊形存璃,不允許多邊形任何一個邊與限制區(qū)域邊界相交超過一個點憎兽。
先將限制邊界轉(zhuǎn)為turf下的linestring和polygon要素勿决。簡稱TL要素和TP要素愿棋。
主要實現(xiàn)思路是通過openlayers的Draw方法控制繪制點不超出限制邊界,再通過地圖單擊事件拿到鼠標(biāo)點下后的點诈嘿,將該點轉(zhuǎn)為turf的point點要素堪旧,通過turf的booleanPointInPolygon方法判斷該點是否在TP要素內(nèi),如果在TP要素內(nèi)并且該點與上一次點擊的點不是同一個點就記錄下該點奖亚,存儲到數(shù)組中 淳梦。通過當(dāng)前點擊的點與上一個點生成連線,通過turf將兩點生成的連線轉(zhuǎn)為truf的linestring要素昔字,再將限制邊界也轉(zhuǎn)為truf下的linestring要素爆袍,通過truf的lineIntersect判斷兩條linestring要素是否相交,如果相交點超過兩個作郭,通過removeLastPoint方法刪除最后繪制的點并且將其移出數(shù)組陨囊,以此達到多邊形不超出限制區(qū)域的目的。同理夹攒,地圖雙擊事件即是將鼠標(biāo)點擊下的點與上一個點和繪制的第一個點生成連線蜘醋,判斷相交點不超過兩個,達到目的
代碼部分
<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(); // 固定限制區(qū)域圖層
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, // 坐標(biāo)系芹助,有EPSG:4326和EPSG:3857
center: props.center, // 中心點坐標(biāo)
minZoom: props.zoomMin || 4, // 地圖縮放最小級別
extent:projectionExtent, // 區(qū)域限制
zoom: props.defaultZoom // 地圖縮放級別(打開頁面時默認級別)
}),
controls: defaultControls({
zoom: false,
rotate: false,
attribution: false
})
})
/** 添加固定限制區(qū)域 */
limitationLayer.value = addJsonVectorLayer('' ,9)
/** 固定限制區(qū)域樣式 */
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] })
/** 轉(zhuǎn)換為turf的lineString圖層 */
linestring.value = turf.lineString(limitationLayer.value.getSource().getFeatures()[0].getGeometry().getCoordinates()[0][0])
/** 轉(zhuǎn)換為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)
/** 地圖單擊事件堂湖,用于在限制區(qū)域內(nèi)打點,判斷當(dāng)前點和上一個點與當(dāng)前點的連線是否超出限制 */
map.value.on('click', mapClick());
/** 地圖雙擊事件状土,雙擊結(jié)束繪制 */
map.value.on('dblclick', mapDoubleClick())
/** 監(jiān)聽開始繪制事件 */
draws.value.addEventListener('drawstart',(evt) => {
evtList.value = evt
drawArr.value = []
modifys.value.setActive(false)
})
/** 監(jiān)聽結(jié)束繪制事件 */
draws.value.addEventListener('drawend',(evt) => {
draws.value.setActive(false)
modifys.value.setActive(true)
drawArr.value = []
controlBool.value = false
})
let prevControl = null // 記錄上一步編輯成功的圖層狀態(tài)
/** 監(jiān)聽開始編輯事件 */
modifys.value.on('modifystart', (e) => {
prevControl = drawLayer.value.getSource().getFeatures()[0].clone()
})
/** 監(jiān)聽結(jié)束編輯事件 */
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('當(dāng)前編輯的圖形超出限制區(qū)域无蜂,請重新編輯!')
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('當(dāng)前編輯的圖形超出限制區(qū)域蒙谓,請重新編輯斥季!')
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('當(dāng)前編輯的圖形超出限制區(qū)域,請重新編輯累驮!')
drawLayer.value.getSource().getFeatures()[0].setGeometry(prevControl.getGeometry())
}
}
})
// draws.value.setActive(false) //TODO 關(guān)閉繪制功能酣倾,可在初始化時關(guān)閉,自己控制是否繪制
// modifys.value.setActive(false) //TODO 關(guān)閉編輯功能谤专,可在初始化時關(guān)閉躁锡,自己控制是否編輯
}
/** 地圖單擊事件 */
function mapClick() {
return function(event) {
// 獲取點擊的坐標(biāo)
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('繪制圖形超出限制區(qū)域,請重新繪制置侍!')
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('繪制圖形超出限制區(qū)域映之,請重新繪制拦焚!')
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 選項如下,更多圖層查閱天地圖官方網(wǎng)站
* 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
}
/** 創(chuàng)建空矢量圖層 */
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('繪制圖形超出限制區(qū)域赎败,請重新繪制!')
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;
}
/** 創(chuàng)建圖層樣式 */
function createStyle(styleConfig){
/**
* 根據(jù) json 格式蠢甲,創(chuàng)建樣式僵刮,并返回 Style 對象
*/
styleConfig = styleConfig || {};
// 創(chuàng)建圖層樣式
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>