在 Vue.js 中集成 OpenLayers 地图:分步指南
介绍
设置应用程序
把地图给我!
修改对象
是时候进行检查了
结论
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
封面艺术由Donato Giacola创作。
介绍
嗨!你很可能至少听说过Vue.js ,这是一个非常流行的前端 JavaScript 框架。它以易于上手、文档齐全和易于理解而著称。
另一方面,你可能听说过也可能没听说过OpenLayers,它是最古老的网络地图库之一。这很正常,地图有时会出乎意料地复杂,而且并非每个人都乐意深入研究这种复杂性,毕竟像Google Maps API这样的服务已经让一切变得简单得多。但请记住,网络地图库的功能远不止在地图上显示标记!
请注意,还有其他类似的库(可参阅这篇文章快速了解)。我们将坚持使用 OpenLayers,因为它从长远来看提供了最多的可能性。
本文将深入探讨 Vue.js 和 OpenLayers 的工作原理,以及如何在 Vue 应用中添加交互式地图并使其真正发挥作用!文章最后,我们将构建一个简单的地理空间对象编辑器,它可以让我们:
- 修改 GeoJSON 格式的对象,并查看它在地图上的显示效果。
- 直接在地图上编辑对象几何形状
设置应用程序
网上已经有很多关于如何搭建 Vue 应用的教程,所以我们就跳过这部分。使用Vue CLI本身就非常简单,调用一下vue create my-app就能完成大部分工作。
从现在开始,我们假设我们有一个简单的应用程序,其主页分为三个框架:
App.vue
<template>
<div id="app">
<div class="cell cell-map">
Map
</div>
<div class="cell cell-edit">
Edit
</div>
<div class="cell cell-inspect">
Inspect
</div>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
html, body {
height: 100%;
margin: 0;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
height: 100%;
display: grid;
grid-template-columns: 100vh;
grid-auto-rows: 1fr;
grid-gap: 1rem;
padding: 1rem;
box-sizing: border-box;
}
.cell {
border-radius: 4px;
background-color: lightgrey;
}
.cell-map {
grid-column: 1;
grid-row-start: 1;
grid-row-end: 3;
}
.cell-edit {
grid-column: 2;
grid-row: 1;
}
.cell-inspect {
grid-column: 2;
grid-row: 2;
}
</style>
这三个框架分别命名为“地图”、“编辑”和“检查”。请注意我们是如何使用CSS Grid来实现布局的。以下是最终效果:
好!接下来我们将按以下步骤进行:先创建第一个Map组件,然后创建Edit第二个组件,最后创建Inspect第三个组件。
把地图给我!
我们来创建一个MapContainer组件,并将其包含在主应用程序中。这里我们需要用到 OpenLayers,所以请记住先安装它:
npm install --save ol
然后创建 Vue 组件:
MapContainer.vue
<template>
<div ref="map-root"
style="width: 100%; height: 100%">
</div>
</template>
<script>
import View from 'ol/View'
import Map from 'ol/Map'
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'
// importing the OpenLayers stylesheet is required for having
// good looking buttons!
import 'ol/ol.css'
export default {
name: 'MapContainer',
components: {},
props: {},
mounted() {
// this is where we create the OpenLayers map
new Map({
// the map will be created using the 'map-root' ref
target: this.$refs['map-root'],
layers: [
// adding a background tiled layer
new TileLayer({
source: new OSM() // tiles are served by OpenStreetMap
}),
],
// the map view will initially show the whole world
view: new View({
zoom: 0,
center: [0, 0],
constrainResolution: true
}),
})
},
}
</script>
这越来越有意思了。你看,用 OpenLayers 创建地图可不是一行代码就能搞定的:你必须提供一个或多个Layer对象,还要给它分配一个属性View。你注意到constrainResolution: true地图视图的选项了吗?这只是为了确保地图缩放级别能够正确对齐,从而使 OSM 图块看起来清晰锐利(更多信息请参阅API 文档)。
ref另请注意,我们使用Vue 指令保留了对地图根目录的引用,如下所示:
<div ref="map-root"
Map构造函数可以接受CSS选择器或实际的HTML元素,因此我们只需使用以下方法获取地图根元素即可this.$refs['map-root']。
结果应该如下所示:
好了,我们有了一张地图,它是交互式的,但除此之外就没什么了。当然,它包含了整个世界,但除此之外……我们不如在上面添加一个物体?
MapContainer.vue
<script>
// ...
// we’ll need these additional imports
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import GeoJSON from 'ol/format/GeoJSON'
// this is a simple triangle over the atlantic ocean
const data = {
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [
[
[
-27.0703125,
43.58039085560784
],
[
-28.125,
23.563987128451217
],
[
-10.8984375,
32.84267363195431
],
[
-27.0703125,
43.58039085560784
]
]
]
}
};
export default {
// ...
mounted() {
// a feature (geospatial object) is created from the GeoJSON
const feature = new GeoJSON().readFeature(data, {
// this is required since GeoJSON uses latitude/longitude,
// but the map is rendered using “Web Mercator”
featureProjection: 'EPSG:3857'
});
// a new vector layer is created with the feature
const vectorLayer = new VectorLayer({
source: new VectorSource({
features: [feature],
}),
})
new Map({
// ...
layers: [
new TileLayer({
source: new OSM(),
}),
// the vector layer is added above the tiled OSM layer
vectorLayer
],
// ...
})
}
}
</script>
简单的!
修改对象
我们当前显示的对象是用GeoJSON 格式表示的。好消息是,这种格式很容易手动编辑!让我们创建一个新组件来实现这一点。
Edit.vue
<template>
<textarea v-model="geojsonEdit"></textarea>
</template>
<script>
export default {
name: 'Edit',
props: {
geojson: Object
},
computed: {
geojsonEdit: {
set(value) {
// when the text is modified, a `change` event is emitted
// note: we’re emitting an object from a string
this.$emit('change', JSON.parse(value))
},
get() {
// the text content is taken from the `geojson` prop
// note: we’re getting a string from an object
return JSON.stringify(this.geojson, null, ' ')
}
}
}
}
</script>
<style>
textarea {
width: 100%;
height: 100%;
resize: none;
}
</style>
该组件接受一个geojsonprop 作为输入,该 prop 将与传递给组件的 prop 相同MapContainer。
好的,现在我们快速看一下 Vue 的一些逻辑。
v-model="geojsonEdit"组件模板中的属性是一个Vue指令,常用于表单输入。它定义了与本地属性的双向数据绑定geojsonEdit,这意味着用户的任何输入都会保存到该属性中,并且对该属性的任何更改都会反映在屏幕上。
为了使该组件生效,我们需要在 GeoJSON 文本被修改时通知父组件。为此,我们将分发一个事件,在 Vue 中可以这样实现:
this.$emit('change', JSON.parse(value))
v-on可以使用指令在父组件中捕获此类事件:
v-on:change="doSomethingWithData($event)"
请注意,它既v-on适用于自定义事件,也适用于标准 HTML5 事件。更多详情请参阅Vue 事件指南。
那么,我们如何知道何时触发该change事件呢?最直接的办法是设置一个监听器geojsonEdit,并在其发生变化时触发事件。
上面的代码采用了另一种解决方案:定义一个计算属性。在这种情况下,使用计算属性非常有用,因为它允许我们指定两种不同的行为(读取和写入),而无需使用监听器。这样,我们只需在set()方法中触发事件,并在方法中读取输入数据即可get()。此外,我们无需在组件中维护任何内部状态,这从长远来看总是有益的。
现在我们回到正题。其他组件目前还无法处理空间对象的更新,因为它目前是硬编码在MapContainer组件中的。
我们将修改这两个MapContainer组件App,以处理不同的数据:
MapContainer.vue
<script>
// ...
export default {
name: 'MapContainer',
components: {},
props: {
// the GeoJSON data is now taken as an input
geojson: Object
},
data: () => ({
// store OL objects on the component instance
olMap: null,
vectorLayer: null
}),
mounted() {
this.vectorLayer = new VectorLayer({
source: new VectorSource({
features: [], // the vector layer is now created empty
}),
})
this.olMap = new Map({
// ..
})
// we’re calling `updateSource` to show the object initially
this.updateSource(this.geojson)
},
watch: {
geojson(value) {
// call `updateSource` whenever the input changes as well
this.updateSource(value)
}
},
methods: {
// this will parse the input data and add it to the map
updateSource(geojson) {
const view = this.olMap.getView()
const source = this.vectorLayer.getSource()
const features = new GeoJSON({
featureProjection: 'EPSG:3857',
}).readFeatures(geojson)
source.clear();
source.addFeatures(features);
// this zooms the view on the created object
view.fit(source.getExtent())
}
}
}
</script>
App.vue
<template>
<div id="app">
<div class="cell cell-map">
<!-- the GeoJSON data is now given as input -->
<MapContainer :geojson="geojson"></MapContainer>
</div>
<div class="cell cell-edit">
<!-- update the app state on `change` events -->
<Edit :geojson="geojson" v-on:change="geojson = $event">
</Edit>
</div>
<div class="cell cell-inspect">
Inspect
</div>
</div>
</template>
<script>
import MapContainer from './components/MapContainer'
import Edit from './components/Edit'
export default {
name: 'App',
components: {
Edit,
MapContainer
},
data: () => ({
// this is the initial GeoJSON data
geojson: {
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [
[
[
-27.0703125,
43.58039085560784
],
[
-28.125,
23.563987128451217
],
[
-10.8984375,
32.84267363195431
],
[
-27.0703125,
43.58039085560784
]
]
]
}
}
})
}
</script>
对组件的修改App非常简单。我们实际上只是将数据存储在这一层而不是上一层MapContainer,并将其作为输入传递给两个子组件。
至于MapContainer,修改稍微复杂一些,但也没复杂多少:通过监视geojson输入属性,我们确保OpenLayers 地图与 Vue 组件状态保持同步。
结果应该如下所示:
此外,地图视图现在会自动放大显示对象!但最棒的是,如果您更改右侧的 GeoJSON 定义……对象会实时更新!是不是很棒?
是时候进行检查了
空间要素通常包含一系列所谓的属性,本质上是键值对。在 GeoJSON 中,您可以将一些属性添加到properties要素的字段中。这些属性有时会显示在屏幕上(例如标签),但通常大多数是“隐藏”的,仅在工具提示或类似界面中显示。
为了进一步推进练习,我们创建一个新Inspect组件,该组件将显示当前指针下方特征的所有属性。
首先,我们要让组件在指针指向的特征被找到时MapContainer发出一个事件:select
MapContainer.vue
<script>
// ...
export default {
// ...
mounted() {
// ...
this.olMap = new Map({
// ...
})
// this binds a callback to the `pointermove` event
this.olMap.on('pointermove', (event) => {
// will return the first feature under the pointer
const hovered = this.olMap.forEachFeatureAtPixel(
event.pixel,
(feature) => feature
)
// emit a `select` event, either with a feature or without
this.$emit('select', hovered)
})
this.updateSource(this.geojson)
},
// ...
}
</script>
同样,我们将发出一个自定义事件,然后使用v-on:select="..."父组件捕获该事件。
此外,我们使用了一种forEachFeatureAtPixel查找光标下特征的方法,该方法会在任何图层中查找特定像素处的所有特征,并对每个特征应用给定的回调函数。在本例中,我们只需要一个特征,因此在找到第一个匹配项后就退出(因为回调函数返回真值)。(feature) => feature
接下来,我们可以创建Inspect组件,该组件将显示该功能的所有属性:
Inspect.vue
<template>
<ul>
<!-- loop on the feature’s attributes -->
<li :key="prop" v-for="prop in props">
<b>{{prop}}:</b> {{feature.get(prop)}}
</li>
</ul>
</template>
<script>
import Feature from 'ol/Feature'
export default {
name: 'Inspect',
props: {
// the only input is an OpenLayers Feature instance
feature: Feature
},
computed: {
// this will return an empty array if no feature available
props() {
return this.feature
? this.feature
.getKeys()
.filter(key => key !== this.feature.getGeometryName())
: []
}
}
}
</script>
<style>
ul {
list-style: none;
}
</style>
注意这行代码this.feature.getKeys().filter(key => key !== this.feature.getGeometryName())吗?它会返回要素所有属性的键,但不包括包含几何图形的属性(因为那样会使数据难以阅读,您可以尝试一下)。更多信息请参阅 OpenLayers要素 API 文档。
最后,让我们把App组件中的所有内容粘合在一起:
App.vue
<template>
<div id="app">
<div class="cell cell-map">
<!-- update app state when a feature is selected -->
<MapContainer :geojson="geojson"
v-on:select="selected = $event">
</MapContainer>
</div>
<div class="cell cell-edit">
<Edit :geojson="geojson" v-on:change="geojson = $event">
</Edit>
</div>
<div class="cell cell-inspect">
<!-- give the selected feature as input -->
<Inspect :feature="selected"></Inspect>
</div>
</div>
</template>
<script>
import MapContainer from './components/MapContainer'
import Edit from './components/Edit'
import Inspect from './components/Inspect'
export default {
name: 'App',
components: {
Inspect,
Edit,
MapContainer
},
data: () => ({
// the selected feature is part of the app state
selected: undefined,
geojson: {
// ...
}
})
}
</script>
好了,差不多就是这样了。现在,将光标悬停在空间对象上,即可显示该对象的属性列表:
您可以尝试向 GeoJSON 对象定义添加属性,看看它们在检查框中是如何显示的!
如果你愿意,也可以尝试复制粘贴像这样的GeoJSON 文件(其中包含所有国家/地区的简化形状)。毕竟,我们对 GeoJSON 数据的内容没有任何假设!
请注意,为了获得更好的性能,还有一些优化需要进行,特别是要确保MapContainer组件仅在必要时发出事件(即不要在每个pointermove事件上都发出相同的功能)。
结论
在本教程中,我们成功创建了一个简单的应用程序,该应用程序允许编辑和检查空间对象。考虑到我们只需要三个组件(每个组件的职责有限)和一个用于存储应用程序状态的根组件,这并不难。
希望这有助于阐明一些Vue.js 的核心概念以及OpenLayers 的概念。
您可以在这里查看最终源代码,其中包含一些对本教程的改进。
希望这篇教程对您有所帮助。我们还可以以此为基础进行更多拓展,如果您有任何想法,请告诉我!感谢阅读!
文章来源:https://dev.to/camptocamp-geo/integrating-an-openlayers-map-in-vue-js-a-step-by-step-guide-2n1p





