vue3 + openlayers實現(xiàn)在固定限制區(qū)域內(nèi)繪制和編輯多邊形

實現(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ù)組,以此達(dá)到多邊形不超出限制區(qū)域的目的。同理,地圖雙擊事件即是將鼠標(biāo)點擊下的點與上一個點和繪制的第一個點生成連線,判斷相交點不超過兩個,達(dá)到目的

代碼部分
<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     // 地圖縮放級別(打開頁面時默認(rèn)級別)
        }),
        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>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容