8 OpenLayers学习笔记-地图裁剪

474 阅读6分钟

需求

只展示某一个省份区域内的地图。

实现过程

对地图进行裁剪,需要GeoJSON文件确定一个裁剪范围,保留GeoJSON范围内部的地图展示,外部的裁剪掉。主要有两个关键,一个是通过mainLayer.setExtent()设置图层范围。另一个是通过监听图层渲染完成后的事件postrender,依据GeoJSON的范围对图层进行裁剪。

知识点

  • layer.getSource().getExtent()用于获取图层源(Layer Source)的地理范围(Extent)的方法。这个方法返回图层源当前包含的要素的地理范围,以一个包含四个数字的数组 [minx, miny, maxx, maxy] 表示。

  • layer.setExtent用于设置地图的当前视图范围。

  • new ol.format.GeoJSON().readFeatures()用于读取GeoJSON数据作为feature,第一个参数是GeoJSON数据,第二个是配置项,可配置投影方式。

  • map.getView().getProjection().getCode()可获取地图的投影方式。

  • 图层的postrender事件,图层渲染完成后的事件。

  • ol.render.getVectorContext(e)用于获取矢量绘制上下文,epostrender事件的参数,包含渲染上下文和其他相关信息。

  • e.context.globalCompositeOperation = 'destination-in',设置渲染上下文的全局合成操作为 'destination-in'。这会将后绘制的内容限制在先前绘制的内容内部,实现裁剪效果。

  • vectorContext.drawFeature(feature, style)使用指定的样式(style)绘制要素。绘制时会应用之前设置的合成操作,从而实现裁剪效果。

  • e.context.globalCompositeOperation = 'source-over',恢复渲染上下文的全局合成操作为 'source-over',使后续绘制不受限制。

  • globalCompositeOperation属于canvas的属性设置,常用值如下:

    • source-over:默认值。新绘制的图形覆盖在已有内容之上。
    • source-in:新绘制的图形只保留与已有内容重叠部分,其他部分被裁剪掉。
    • source-out:新绘制的图形只保留与已有内容不重叠部分,重叠部分被裁剪掉。
    • source-atop:新绘制的图形与已有内容重叠部分绘制在已有内容之上,不重叠部分保留。
    • destination-over:新绘制的图形位于已有内容之下。
    • destination-in:已有内容被新绘制的图形裁剪,只保留与新绘制的图形重叠部分。
    • destination-out:已有内容只保留与新绘制的图形不重叠部分,重叠部分被裁剪掉。
    • destination-atop:已有内容与新绘制的图形重叠部分绘制在新绘制的图形之上,不重叠部分保留。
    • lighter:新绘制的图形与已有内容叠加,产生更亮的颜色。
    • copy:只显示新绘制的图形,已有内容被忽略。
    • xor:新绘制的图形与已有内容进行异或操作。
  • 对于drawFeature()设置的style,其中配置了color参数,这个参数的值,设置为多少无所谓。gpt给出的解释是:在OpenLayers中,设置颜色为黑色的填充样式通常用于裁剪效果。在裁剪地图时,通过将渲染上下文的全局合成操作设置为 'destination-in',然后使用黑色填充样式绘制裁剪区域,可以实现将地图的显示限制在裁剪区域内的效果。

    设置填充样式为黑色的目的不是为了让裁剪区域显示为黑色,而是为了利用合成操作的特性,将后绘制的内容限制在先前绘制的内容内部。实际上,裁剪区域将不会显示为黑色,而是在地图中实现了一种裁剪的效果,只有裁剪区域内的内容会被显示,裁剪区域外的内容将被隐藏。

    因此,设置填充样式为黑色只是为了使用这种合成操作的特性,并不会直接导致裁剪区域呈现黑色。

  • 实现功能后发现一个bug,将只显示的区域,比如黑龙江省区域,完全移出地图范围内时,地图就显示了GeoJSON范围外的地图。裁剪就失效了。在移动地图,使GeoJSON范围出现在地图范围内后,GeoJSON范围外的地图又被裁剪了。官方的demo没有这个问题。经过排查,发现是加载GeoJSON方式不同导致的,如果自己写代码使用xhr或者fetch加载GeoJSON,然后使用new ol.format.GeoJSON().readFeatures(cqBorderGeoJSON, { featureProjection: 'EPSG:3857' })去读取GeoJSON的数据,赋值给features,就会有这个问题。如果使用new ol.source.Vector({url: './230000.json'})的方式去加载GeoJSON,就不会有这个问题。具体什么原因,我没搞清楚。所以尽量使用第二种方式加载GeoJSON。如果使用第一种方式,可在地图的moveend事件内,判断GeoJSON的范围是否完全被移出地图范围,如果移出了,强制回到GeoJSON范围。这个过程会有GeoJSON范围外的地图闪烁一下的问题,可通过增加覆盖层等方式规避。

代码HTML+CSS+JS

  • 由于要使用xhr或者fetch加载GeoJSON文件,所以需要以服务的方式访问html文件,电脑安装了node的情况下,推荐安装anywhere(npm i anywhere -g),在任意文件夹内命令行执行anywhere 端口即可以服务的形式访问这个文件夹。或者使用vscode等编译器自带的服务。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/8.2.0/ol.min.css" integrity="sha512-bc9nJM5uKHN+wK7rtqMnzlGicwJBWR11SIDFJlYBe5fVOwjHGtXX8KMyYZ4sMgSL0CoUjo4GYgIBucOtqX/RUQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <title>地图裁剪</title>
    <style>
        * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }

    html,
    body,
    #app,
    .app-map {
        height: 100%;
    }

    .app-btns {
        position: fixed;
        right: 10px;
        top: 10px;
        background-color: #fff;
        box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
        width: 210px;
        padding: 25px;
        text-align: center;
        border-radius: 5px;
        display: flex;
        flex-direction: column;
    }

    .app-btns button {
        font-size: 18px;
        border: none;
        padding: 12px 20px;
        border-radius: 4px;
        color: #fff;
        background-color: #409eff;
        border-color: #409eff;
        cursor: pointer;
        border: 1px solid #dcdfe6;
        margin-bottom: 5px;
    }
    .app-btns button.active,
    .app-btns button:hover {
        background-color: rgba(7, 193, 96, 0.8);
    }
    </style>
</head>

<body>
    <div id="app">
        <div class="app-map" id="map"></div>
        <div class="app-btns">
            <button type="button" @click='handleClickInitMap(1)' :class='{active: type === 1}'>自己请求JOSN初始化地图</button>
            <button type="button" @click='handleClickInitMap(2)' :class='{active: type === 2}'>ol请求JOSN初始化地图</button>
            <button type="button" @click='handleClickGoToChongQing' v-show='map'>测试跳转到重庆</button>
        </div>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.14/vue.global.prod.min.js" integrity="sha512-huEQFMCpBzGkSDSPVAeQFMfvWuQJWs09DslYxQ1xHeaCGQlBiky9KKZuXX7zfb0ytmgvfpTIKKAmlCZT94TAlQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/8.2.0/dist/ol.min.js" integrity="sha512-+nvfloZUX7awRy1yslYBsicmHKh/qFW5w79+AiGiNcbewg0nBy7AS4G3+aK/Rm+eGPOKlO3tLuVphMxFXeKeOQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script>
    </script>
    <script>
    console.log(`openlayers版本号: ${ol.util.VERSION}`);
    const { createApp } = Vue;
    // 隐藏缩放控件
    const controls = ol.control.defaults.defaults({
        zoom: false
    });
    // 瓦片图层
    const mainLayer = new ol.layer.Tile({
        source: new ol.source.XYZ({
            url: 'http://wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}'
        })
    });
    // 裁剪需要的样式
    const style = new ol.style.Style({
        fill: new ol.style.Fill({
            color: 'black' // 设置为多少无所谓,设置了就行
        })
    });
    const vm = createApp({
            data() {
                return {
                    map: null, // 地图实例
                    type: 0, // 加载JSON的方式
                    hljBorderLayer: null // 边界图层
                }
            },
            methods: {
                // 点击加载GeoJSON并初始化地图
                handleClickInitMap(type) {
                    if (this.map) {
                        this.map.setTarget(null);
                        this.map = null;
                    }
                    this.type = type;
                    if (type === 1) {
                        this.getJSON1();
                    } else {
                        this.getJSON2();
                    }
                },
                // fetch请求json, 初始化地图
                async getJSON1() {
                    // 请求边界
                    const cqBorderGeoJSONRes = await fetch(`./230000.json`);
                    const cqBorderGeoJSON = await cqBorderGeoJSONRes.json();
                    // 创建边界图层
                    this.hljBorderLayer = new ol.layer.Vector({
                        style: new ol.style.Style({
                            stroke: new ol.style.Stroke({
                                color: '#07c160',
                                width: 3
                            })
                        }),
                        source: new ol.source.Vector({
                            features: new ol.format.GeoJSON().readFeatures(cqBorderGeoJSON, { featureProjection: 'EPSG:3857' }),
                        })
                    });
                    this.initMap();
                    // 限制GeoJSON的区域不能完全移出屏幕,如移出,自动跳回
                    this.map.on('moveend', () => {
                        const [mapMinX, mapMinY, mapMaxX, mapMaxY] = this.map.getView().calculateExtent(this.map.getSize());
                        // 检查 GeoJSON 的四个方向是否都在视图范围之外
                        const [jsonMinX, jsonMinY, jsonMaxX, jsonMaxY] = this.hljBorderLayer.getSource().getExtent();
                        if (mapMinX >= jsonMaxX) {
                            console.log('左侧超出了');
                        };
                        if (mapMaxX <= jsonMinX) {
                            console.log('右侧超出了');
                        };
                        if (mapMaxY <= jsonMinY) {
                            console.log('上侧超出了');
                        };
                        if (mapMinY >= jsonMaxY) {
                            console.log('下侧超出了');
                        };
                        const isOutMap = mapMinX >= jsonMaxX || mapMaxX <= jsonMinX || mapMaxY <= jsonMinY || mapMinY >= jsonMaxY;
                        if (isOutMap) {
                            this.map.getView().animate({
                                center: ol.proj.fromLonLat([127.1715261, 48.4281034]),
                                zoom: 6,
                                duration: 0
                            })
                        }
                    });
                },
                // ol去请求josn,初始化地图
                getJSON2() {
                    this.hljBorderLayer = new ol.layer.Vector({
                        style: new ol.style.Style({
                            stroke: new ol.style.Stroke({
                                color: '#07c160',
                                width: 3
                            })
                        }),
                        source: new ol.source.Vector({
                            url: './230000.json',
                            format: new ol.format.GeoJSON(),
                        }),
                    });
                    this.initMap();
                },
                // 初始化地图
                initMap() {
                    // Giving the clipped layer an extent is necessary to avoid rendering when the feature is outside the viewport
                    // 官方的注释,貌似是为了避免范围外的地图渲染
                    this.hljBorderLayer.getSource().on('addfeature', () => {
                        mainLayer.setExtent(this.hljBorderLayer.getSource().getExtent());
                    });
                    // 地图渲染完成后触发
                    mainLayer.on('postrender', (e) => {
                        const vectorContext = ol.render.getVectorContext(e);
                        e.context.globalCompositeOperation = 'destination-in';
                        this.hljBorderLayer.getSource().forEachFeature(function(feature) {
                            vectorContext.drawFeature(feature, style);
                        });
                        e.context.globalCompositeOperation = 'source-over';
                    });
                    // 初始化地图
                    this.map = new ol.Map({
                        layers: [mainLayer, this.hljBorderLayer],
                        target: 'map',
                        view: new ol.View({
                            center: ol.proj.fromLonLat([127.1715261, 48.4281034]),
                            zoom: 6
                        }),
                        controls
                    });
                },
                // 跳转到重庆
                handleClickGoToChongQing() {
                    this.map.getView().animate({
                        center: ol.proj.fromLonLat([107.768711, 30.096964]),
                        zoom: 6
                    })
                }
            },
            mounted() {
                this.handleClickInitMap(1);
            }
        }).mount('#app');
    </script>
</body>

</html>

参考文章

地图裁剪官方栗子

postrender官方文档

getVectorContext官方文档

globalCompositeOperation官方文档

GeoJSON文件可以在阿里云网站获取