vue3 + openlayers实现在固定限制区域内绘制和编辑多边形

实现类似于电子围栏的功能,在限制区域内绘制和编辑多边形,不允许多边形任何一个边与限制区域边界相交超过一个点。

先将限制边界转为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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容