vue+openlayers 开发一个卷帘图层控件
卷帘功能是gis项目中常见的一种基础功能,主要用于数据的叠加比对。在openlayers中实现卷帘图层并不复杂,下面开讲怎么做。
在vue中开发一个openlayers控件
开发openlayers控件主要使用ol/control/Control,openlayers中所有的control都是继承这个类。
这个类接收3个参数 element,target,render,我们关注element和target就可以,render暂时用不到。
new Control({
element: 控件的dom元素,
target: 控件创建位置的dom元素,当需要创建在地图元素之外时才需要设置这个,
})
在vue中,我们在template中创建面板,然后在mount中创建Control,通过refs将dom元素作为参数,就可以创建出一个自定义控件。
<template>
<div v-show="false">
<div ref="myControl">...</div>
</div>
</template>
<script>
import Control from "ol/control/Control"
export default {
...
mounted(){
// 创建openlayers的Map和view
...
let myControl = new Control({
element: this.$refs["myControl"],
})
map.addControl(myControl)
}
}
</script>
卷帘功能原理
卷帘功能由两个部分组成,操作面板(分割线和按钮)和图层。
逻辑是按钮按下时拖动分割线,根据分割线当前位置控制图层显示部分。
先随便创建一个地图并添加一个图层
从官网示例复制的,数据服务都是公开的。
<template>
<div>
<div id="map"></div>
</div>
</template>
<script>
import "ol/ol.css"
import Map from "ol/Map"
import XYZ from "ol/source/XYZ"
import TileLayer from "ol/layer/Tile"
import View from "ol/View"
import GeoJSON from 'ol/format/GeoJSON.js';
import VectorLayer from 'ol/layer/Vector.js';
import VectorSource from 'ol/source/Vector.js';
import {Fill, Style} from 'ol/style.js';
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {
const layer = new TileLayer({
source: new XYZ({
attributions: 'Tiles © <a href="https://services.arcgisonline.com/ArcGIS/' + 'rest/services/World_Topo_Map/MapServer">ArcGIS</a>',
url: "https://server.arcgisonline.com/ArcGIS/rest/services/" + "World_Topo_Map/MapServer/tile/{z}/{y}/{x}",
}),
})
const style = new Style({
fill: new Fill({
color: "#eeeeee",
}),
})
const vectorLayer = new VectorLayer({
source: new VectorSource({
url: "https://openlayers.org/data/vector/ecoregions.json",
format: new GeoJSON(),
}),
style: function (feature) {
const color = feature.get("COLOR") || "#eeeeee"
style.getFill().setColor(color)
return style
},
})
const map = new Map({
layers: [layer, vectorLayer],
target: "map",
view: new View({
center: [0, 0],
zoom: 2,
}),
})
// 为了后续使用方便将map放入this中
this.map = map
},
}
</script>
<style lang="scss">
#map {
width: 1980px;
height: 800px;
}
</style>
控制图层仅显示局部
实现方式与做地图裁切时相同,利用prerender和postrender时间来实现。
我们封装一下,放到method中方便后续调用。
export default {
data() {
return {
swipeLayer: null
}
},
computed: {},
methods: {
setSwipeLayer(layer) {
this.unsetSwipeLayer()
if (layer) {
layer.on("prerender", this.prerender)
layer.on("postrender", this.postrender)
this.swipeLayer = layer
}
},
unsetSwipeLayer() {
if (this.swipeLayer) {
this.swipeLayer.un("prerender", this.prerender)
this.swipeLayer.un("postrender", this.postrender)
this.swipeLayer = null
}
},
prerender(e) {
const ctx = e.context
const hOrw = ctx.canvas.width
const nVal = hOrw * 50 / 100 //仅显示一半
ctx.save()
ctx.beginPath()
ctx.rect(0, 0, nVal, ctx.canvas.height)
ctx.clip() // 裁剪
this.map.render()
},
postrender(e) {
const ctx = e.context
ctx.restore()
},
},
mounted() {
...
this.setSwipeLayer(vectorLayer)
},
}
此时再看地图应该只会显示一半,下面来创建分割线和按钮。
创建分割线和按钮,添加自定义控件
创建一个不可见的div来存放控件UI,这样在页面初始化时就不会出现面板闪一下的问题。
<template>
<div>
<div id="map"></div>
<div v-show="false">
<div
class="ol-control ol-unselectable my-swipe"
:style="{pointerEvents: isMoving?'auto':'none'}"
ref="mySwipe"
@mousemove="mouseMove"
@mouseup="mouseUp"
>
<div
class="my-swipe-line swipe-horizontal"
:style="{left:swipeVal+'%',pointerEvents:isMoving?'none':'auto'}"
>
<span
class="my-swipe-line-btn"
:style="{pointerEvents:isMoving?'none':'auto'}"
@mousedown="mouseDown"
>
<img :src="require('./change.png')" alt="拖动" />
</span>
</div>
</div>
</div>
</div>
</template>
<script>
...
import Control from "ol/control/Control"
export default {
data() {
return {
swipeLayer: null,
isMoving: false,
swipeVal: 50,
}
},
computed: {},
methods: {
...
mouseMove(e) {
if (!this.isMoving) return
const clientWidth = e.currentTarget.clientWidth
const clientHeight = e.currentTarget.clientHeight
const x = e.offsetX
const y = e.offsetY
if (x < 0 || x > clientWidth || y < 0 || y > clientHeight) return
this.swipeVal = (x / clientWidth) * 100
},
mouseDown(e) {
this.isMoving = true
},
mouseUp(e) {
this.isMoving = false
},
},
mounted() {
...
// 创建卷帘控件
this.swipeControl = new Control({
element: this.$refs["mySwipe"],
})
map.addControl(this.swipeControl)
},
}
</script>
<style lang="scss">
...
.my-swipe {
width: 100%;
height: 100%;
z-index: 110;
background: none !important;
padding: 0;
pointer-events: none;
.my-swipe-line {
position: absolute;
width: 1px;
height: 100%;
background: white;
.my-swipe-line-btn {
position: relative;
display: block;
top: 50%;
width: 20px;
height: 30px;
margin-left: -10px;
background: white;
border: 1px #efefef solid;
border-radius: 3px;
cursor: col-resize;
img {
vertical-align: middle;
border-style: none;
}
}
}
}
</style>
这一步创建出了控制界面,可通过鼠标拖动分割线。
将控件和图层关联起来
得益于vue的数据绑定,只需要修改prerender方法中的nVal部分就可以了。
export default {
data() {
return {
swipeLayer: null
}
},
computed: {},
methods: {
prerender(e) {
...
const nVal = hOrw * this.swipeVal / 100 //根据分割线显示
...
},
},
mounted() {
...
},
}
完整代码
<template>
<div>
<div id="map"></div>
<div v-show="false">
<div
class="ol-control ol-unselectable my-swipe"
:style="{pointerEvents: isMoving?'auto':'none'}"
ref="mySwipe"
@mousemove="mouseMove"
@mouseup="mouseUp"
>
<div
class="my-swipe-line swipe-horizontal"
:style="{left:swipeVal+'%',pointerEvents:isMoving?'none':'auto'}"
>
<span
class="my-swipe-line-btn"
:style="{pointerEvents:isMoving?'none':'auto'}"
@mousedown="mouseDown"
>
<img :src="require('./change.png')" alt="拖动" />
</span>
</div>
</div>
</div>
</div>
</template>
<script>
import "ol/ol.css"
import Map from "ol/Map"
import XYZ from "ol/source/XYZ"
import TileLayer from "ol/layer/Tile"
import View from "ol/View"
import GeoJSON from "ol/format/GeoJSON.js"
import VectorLayer from "ol/layer/Vector.js"
import VectorSource from "ol/source/Vector.js"
import { Fill, Style } from "ol/style.js"
import Control from "ol/control/Control"
export default {
data() {
return {
swipeLayer: null,
isMoving: false,
swipeVal: 50,
}
},
computed: {},
methods: {
setSwipeLayer(layer) {
this.unsetSwipeLayer()
if (layer) {
layer.on("prerender", this.prerender)
layer.on("postrender", this.postrender)
this.swipeLayer = layer
}
},
unsetSwipeLayer() {
if (this.swipeLayer) {
this.swipeLayer.un("prerender", this.prerender)
this.swipeLayer.un("postrender", this.postrender)
this.swipeLayer = null
}
},
prerender(e) {
const ctx = e.context
const hOrw = ctx.canvas.width
const nVal = (hOrw * this.swipeVal) / 100 //根据分割线显示
ctx.save()
ctx.beginPath()
ctx.rect(0, 0, nVal, ctx.canvas.height)
ctx.clip() // 裁剪
this.map.render()
},
postrender(e) {
const ctx = e.context
ctx.restore()
},
mouseMove(e) {
if (!this.isMoving) return
const clientWidth = e.currentTarget.clientWidth
const clientHeight = e.currentTarget.clientHeight
const x = e.offsetX
const y = e.offsetY
if (x < 0 || x > clientWidth || y < 0 || y > clientHeight) return
this.swipeVal = (x / clientWidth) * 100
},
mouseDown(e) {
this.isMoving = true
},
mouseUp(e) {
this.isMoving = false
},
},
mounted() {
const layer = new TileLayer({
source: new XYZ({
attributions: 'Tiles © <a href="https://services.arcgisonline.com/ArcGIS/' + 'rest/services/World_Topo_Map/MapServer">ArcGIS</a>',
url: "https://server.arcgisonline.com/ArcGIS/rest/services/" + "World_Topo_Map/MapServer/tile/{z}/{y}/{x}",
}),
})
const style = new Style({
fill: new Fill({
color: "#eeeeee",
}),
})
const vectorLayer = new VectorLayer({
source: new VectorSource({
url: "https://openlayers.org/data/vector/ecoregions.json",
format: new GeoJSON(),
}),
style: function (feature) {
const color = feature.get("COLOR") || "#eeeeee"
style.getFill().setColor(color)
return style
},
})
const map = new Map({
layers: [layer, vectorLayer],
target: "map",
view: new View({
center: [0, 0],
zoom: 2,
}),
})
// 为了后续使用方便将map放入this中
this.map = map
this.setSwipeLayer(vectorLayer)
// 创建卷帘控件
this.swipeControl = new Control({
element: this.$refs["mySwipe"],
})
map.addControl(this.swipeControl)
},
}
</script>
<style lang="scss">
#map {
width: 1980px;
height: 800px;
}
.my-swipe {
width: 100%;
height: 100%;
z-index: 110;
background: none !important;
padding: 0;
pointer-events: none;
.my-swipe-line {
position: absolute;
width: 1px;
height: 100%;
background: white;
.my-swipe-line-btn {
position: relative;
display: block;
top: 50%;
width: 20px;
height: 30px;
margin-left: -10px;
background: white;
border: 1px #efefef solid;
border-radius: 3px;
cursor: col-resize;
img {
vertical-align: middle;
border-style: none;
}
}
}
}
</style>
延伸
代码逻辑比较简单,实际应用中通常会要求扩展一下功能,比如可以控制分割线哪一侧需要显示,卷动方向是水平还是垂直的,卷动图层要可修改等等。根据实际情况稍微改动一下就可以实现。