需求
只展示某一个省份区域内的地图。
实现过程
对地图进行裁剪,需要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)用于获取矢量绘制上下文,e是postrender事件的参数,包含渲染上下文和其他相关信息。 -
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>