^ 关注我,带你一起学GIS ^
注:当前使用的是 ol [9.2.4] 版本,天地图使用的
key
请到天地图官网申请,并替换为自己的key
在WebGIS
系统开发中,有时候会遇到只需要展示某一图层特殊区域的需求,通常情况下需要单独发一个地图服务,但为了简化操作,可以利用OpenLayers
进行图层遮罩与裁剪处理。
1. 图层遮罩
图层遮罩即为图层加上遮罩层,单独留出展示区域。结合canvas
,通过上下文属性globalCompositeOperation
和绘制方法可以方便的设置遮罩层。
1.1. 创建canvas元素
创建canvas
元素,并将其添加到地图视图作为子元素,设置position
为绝对定位,宽高与父元素相等,然后设置显示层级在父元素之上。
const { offsetWidth, offsetHeight } = map.getViewport()
overCanvas = document.createElement('canvas');
overCanvas.width = offsetWidth;
overCanvas.height = offsetHeight;
overCanvas.style.position = 'absolute';
overCanvas.style.top = '0';
overCanvas.style.left = '0';
overCanvas.style.zIndex = '99';
const ctx = overCanvas.getContext('2d');
map.getViewport().appendChild(overCanvas);
1.2. 设置遮罩层样式
fillStyle
为填充色,strokeStyle
为边线色,lineWidth
为边线宽度。
const fillStyle = 'rgba(255,255,255,1)'
const strokeStyle = 'red'
const lineWidth = 3
ctx.fillStyle = fillStyle;
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth;
1.3. 绘制遮罩层
清除画布:在绘制遮罩层之前需要使用clearRect
方法清除画布。clearRect
方法会将从左上角(0,0)开始,宽高为地图大小的矩形区域设置为透明黑色(rgb(0 0 0 / 0%)
)。坐标转换:使用map.getPixelFromCoordinate
将地理坐标转换为屏幕坐标,对于地理坐标系,可以直接转换,若为投影坐标系,需要使用ol.proj.fromLonLat
将其转为投影坐标,再转换为屏幕坐标。绘制遮罩层:调用beginPath
开始绘制路径,moveTo
表示起点,lineTo
表示绘制路径,绘制完成后使用closePath
关闭路径,然后调用fill
完成填充,第一个形状绘制完成,简称s1
。然后使用rect
绘制矩形,调用fill
进行填充,第二个形状绘制完成,简称s2
。在s1
和s2
之间设置globalCompositeOperation
属性为source-out
。globalCompositeOperatio = 'source-out'
表示新形状为原来的形状s1
和后面的形状s2
不相交的部分。绘制显示范围线:使用stroke
方法进行路径绘制。globalCompositeOperatio = 'source-over'
表示新形状位于旧形状之上,显示层级更高。
// 清除画布
ctx.clearRect(0, 0, overCanvas.width, overCanvas.height)
const ynJSON = YNGeoJSON.features[0].geometry.coordinates[0][0]
const coords = ynJSON.map(coord => {
// return map.getPixelFromCoordinate(ol.proj.fromLonLat(coord))
return map.getPixelFromCoordinate(coord)
})// 绘制显示区域
ctx.beginPath();
coords.forEach((coord, index) => {
index === 0 ? ctx.moveTo(coord[0], coord[1]) : ctx.lineTo(coord[0], coord[1])
})
ctx.closePath();
ctx.fill();// 绘制遮罩层
ctx.globalCompositeOperation = 'source-out';
ctx.rect(0, 0, overCanvas.width, overCanvas.height)
ctx.fill();
// 绘制路径
ctx.globalCompositeOperation = 'source-over';
ctx.beginPath();
coords.forEach((coord, index) => {
index === 0 ? ctx.moveTo(coord[0], coord[1]) : ctx.lineTo(coord[0], coord[1])
})
ctx.closePath();
ctx.stroke()
方式一:先绘制显示区域,再绘制遮罩层,设置ctx.globalCompositeOperation = 'source-out'
。
// 绘制显示区域
ctx.beginPath();
coords.forEach((coord, index) => {
index === 0 ? ctx.moveTo(coord[0], coord[1]) : ctx.lineTo(coord[0], coord[1])
})
ctx.closePath();
ctx.fill();ctx.globalCompositeOperation = 'source-out';
// 绘制遮罩层
ctx.rect(0, 0, overCanvas.width, overCanvas.height)
ctx.fill();
方式二:先绘制遮罩层,再绘制显示区域,设置ctx.globalCompositeOperation = 'destination-out'
。
// 绘制遮罩层
ctx.rect(0, 0, canvas.width, canvas.height)
ctx.fill();ctx.globalCompositeOperation = 'destination-out';
// 绘制显示区域
ctx.beginPath();
coords.forEach((coord, index) => {
index === 0 ? ctx.moveTo(coord[0], coord[1]) : ctx.lineTo(coord[0], coord[1])
})
ctx.closePath();
ctx.fill();
方式二:先绘制遮罩层,再绘制显示区域,设置ctx.globalCompositeOperation = 'xor'
。
// 绘制遮罩层
ctx.fillRect(0, 0, canvas.width, canvas.height)ctx.globalCompositeOperation = "xor";
// 绘制显示区域
ctx.beginPath();
coords.forEach((coord, index) => {
index === 0 ? ctx.moveTo(coord[0], coord[1]) : ctx.lineTo(coord[0], coord[1])
})
ctx.closePath()
ctx.fill();
1.4. 取消遮罩
移除添加的遮罩元素即可取消遮罩。
/**
* 取消遮罩
*/
function cancelOverlay(overCanvas) {
map.getViewport().removeChild(overCanvas);
}
2. 图层裁剪
2.1. 创建裁剪要素
在进行图层裁剪之前需要创建裁剪要素,可以通过GeoJSON
数据构建裁剪要素区域。
const coords = YNGeoJSON.features[0].geometry.coordinates[0]
const clipFeature = new ol.Feature({
geometry: new ol.geom.Polygon(coords),
})
2.2. 开启裁剪
创建图层裁剪方法,传递待裁剪图层和裁剪要素参数。监听裁剪图层渲染前和渲染后事件,在图层渲染前进行裁剪,渲染完成后恢复画布,并设置图层显示范围为裁剪区域。
/**
* 创建裁剪
* @param {ol.layer} clipLayer 需要裁剪的图层
* @param {ol.feature} clipFeature 裁剪区域
*/
function createClipByLayer(clipLayer, clipFeature) {
// 监听图层渲染前事件,进行裁剪操作
clipLayer.on("prerender", prerenderEvt)
// 监听图层渲染后事件,进行画布恢复
clipLayer.on("postrender", postrenderEvt)
// 设置裁剪范围
clipLayer.setExtent(clipFeature.getGeometry().getExtent())
}
**prerenderEvt**
:图层渲染前处理事件
function prerenderEvt(event) {
const vectorContext = ol.render.getVectorContext(event);
event.context.globalCompositeOperation = 'source-over';
const ctx = event.context;
// 保存裁剪前画布状态
ctx.save();
// 绘制裁剪区域
vectorContext.drawFeature(clipFeature, new ol.style.Style({
stroke: new ol.style.Stroke({
color: "red",
width: 5,
}),
fill: new ol.style.Fill({
color: "rgba(0, 0, 255, 0.25)",
})
}));
ctx.clip();
}
**postrenderEvt**
:图层渲染后处理事件
function postrenderEvt(event) {
let ctx = event.context;
// 恢复裁剪前画布状态
ctx.restore();
}
2.3. 取消裁剪
取消裁剪操作前先移除裁剪图层,然后再将其添加到地图中,并取消地图渲染监听事件,最后设置地图全幅范围显示。
/**
* 取消裁剪
* @param {ol.layer} clipLayer 需要裁剪的图层
* @param {ol.layer} fullExtentLayer 地图全图范围图层
*/
function cancelClip(clipLayer,fullExtentLayer) {
map.removeLayer(clipLayer)
map.addLayer(clipLayer)
// 设置图层显示层级
clipLayer.setZIndex(-1)
// 取消图层事件监听
clipLayer.un("prerender", prerenderEvt)
clipLayer.un("postrender", postrenderEvt)
// 设置图层全幅显示
clipLayer.setExtent(fullExtentLayer.getExtent())
}
3. 不成功的例子
在下面的代码中,我想通过监听地图事件precompose
和postcompose
来完成裁剪操作,这样就不用针对每个图层进行裁剪了。但是运行发现并没有达到理想的效果,有两个问题,一是坐标计算不对,应该是地理坐标转换为屏幕坐标不准确;二是裁剪后图层并没有只显示裁剪区域,随着地图放大、缩小或者拖动,其他区域也会显示出来。我暂时也没有想到更好的解决办法,等着以后再探究吧,也希望有想法的小伙伴可以指正一下。
/**
* 开启裁剪
*/
function startClip() {
map.on("precompose", function (event) {
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");ctx.save();
ctx.reset()
ctx.beginPath();
const ynJSON = YNGeoJSON.features[0].geometry.coordinates[0][0]
// ctx.rect(0, 0, canvas.width, canvas.height);
const coords = ynJSON.map(coord => {
return map.getPixelFromCoordinate(ol.proj.fromLonLat(coord,map.getView().getProjection()))
})
coords.forEach((coord, index) => {
// const y = ol.render.getRenderPixel(event,coord[1])
index === 0 ? ctx.moveTo(coord[0], coord[1]) : ctx.lineTo(coord[0], coord[1])
})
ctx.closePath
ctx.clip();ctx.globalCompositeOperation = 'source-over';
ctx.beginPath();
coords.forEach((coord, index) => {
index === 0 ? ctx.moveTo(coord[0], coord[1]) : ctx.lineTo(coord[0], coord[1])
})
ctx.closePath();
ctx.stroke()
});
map.on("postcompose", function (event) {
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.restore()
})
}
4. canvas小知识
现代前端二维地图库,区别于三维地图WebGL
模式,基本上都是利用canvas
进行渲染,所以了解一下canvas相关知识是有必要的。在使用canvas
对象时,要进行其他操作前,必须要先获取canvas
上下文对象,即ctx``const ctx = canvas.getContext("2d")
下面介绍一些绘制方法和属性:
方法名称 |
作用 |
|
创建从左上角 |
|
根据 |
|
根据给定 |
|
绘制从左上角 |
|
根据 |
|
根据坐标 |
|
绘制从上一个点到给定坐标 |
|
裁剪给定区域。由于裁剪的不可恢复性,在裁剪开始前需要使用 |
|
重置绘制路径以及样式等 |
|
保存绘制前的状态,如 |
|
恢复绘制前的状态,如 |
|
设置边线样式,如 |
|
设置填充样式,如 |
**全局合成属性**:`globalCompositeOperation`我将现有画布内容简称为旧形状,新绘制对象为新形状。
属性值 |
作用 |
默认设置,绘制的新形状在旧形状之上 |
|
新形状仅在新形状和目标画布重叠的地方绘制。其他透明。 |
|
新形状在新形状与旧形状没有重叠的地方进行绘制(绘制非重叠区域) |
|
新形状仅绘制与旧形状重叠的区域(绘制重叠区域) |
|
绘制的新形状在旧形状之下 |
|
旧形状仅在新形状和目标画布重叠的地方绘制。其他透明。 |
|
绘制旧形状与新形状的非重叠区域 |
|
现有画布仅保留与新形状重叠的区域。新形状绘制在画布内容后面。 |
|
只显示新形状 |
|
新旧形状相交的区域为透明,其他区域正常显示 |
5. 参考知识
牛老师:https://mp.weixin.qq.com/s/Jv5zhxG1wAoJUzDBe8XsrAcanvas:https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Compositing
OpenLayers:https://openlayers.org/en/v9.2.4/examples/layer-clipping-vector.html
❝
OpenLayers示例数据下载,请在公众号后台回复:ol数据
全国信息化工程师-GIS 应用水平考试资料,请在公众号后台回复:GIS考试
❝
GIS之路公众号已经接入了智能助手,欢迎大家前来提问。
欢迎访问我的博客网站-长谈GIS:
http://shanhaitalk.com
都看到这了,不要忘记点赞、收藏+关注 哦!
本号不定时更新有关 GIS开发 相关内容,欢迎关注