发布于 2026-01-06 1 阅读
0

在 Vue.js 中集成 OpenLayers 地图:分步指南 简介 设置应用程序 获取地图! 修改对象 检查时间 结论 DEV 全球展示挑战赛 由 Mux 呈现:展示你的项目!

在 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>


Enter fullscreen mode Exit fullscreen mode

这三个框架分别命名为“地图”“编辑”“检查”。请注意我们是如何使用CSS Grid来实现布局的。以下是最终效果:

3个灰色相框

好!接下来我们将按以下步骤进行:先创建第一个Map组件,然后创建Edit第二个组件,最后创建Inspect第三个组件。

把地图给我!

我们来创建一个MapContainer组件,并将其包含在主应用程序中。这里我们需要用到 OpenLayers,所以请记住先安装它:



npm install --save ol


Enter fullscreen mode Exit fullscreen mode

然后创建 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>


Enter fullscreen mode Exit fullscreen mode

这越来越有意思了。你看,用 OpenLayers 创建地图可不是一行代码就能搞定的:你必须提供一个或多个Layer对象,还要给它分配一个属性View。你注意到constrainResolution: true地图视图的选项了吗?这只是为了确保地图缩放级别能够正确对齐,从而使 OSM 图块看起来清晰锐利(更多信息请参阅API 文档)。

ref另请注意,我们使用Vue 指令保留了对地图根目录的引用,如下所示:



<div ref="map-root"


Enter fullscreen mode Exit fullscreen mode

Map构造函数可以接受CSS选择器或实际的HTML元素,因此我们只需使用以下方法获取地图根元素即可this.$refs['map-root']

结果应该如下所示:

OSM世界地图

好了,我们有了一张地图,它是交互式的,但除此之外就没什么了。当然,它包含了整个世界,但除此之外……我们不如在上面添加一个物体?

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>


Enter fullscreen mode Exit fullscreen mode

简单的!

三角形的 OSM 地图

修改对象

我们当前显示的对象是用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>


Enter fullscreen mode Exit fullscreen mode

该组件接受一个geojsonprop 作为输入,该 prop 将与传递给组件的 prop 相同MapContainer

好的,现在我们快速看一下 Vue 的一些逻辑。

v-model="geojsonEdit"组件模板中的属性是一个Vue指令,常用于表单输入。它定义了与本地属性的双向数据绑定geojsonEdit,这意味着用户的任何输入都会保存到该属性中,并且对该属性的任何更改都会反映在屏幕上。

为了使该组件生效,我们需要在 GeoJSON 文本被修改时通知父组件。为此,我们将分发一个事件,在 Vue 中可以这样实现:



this.$emit('change', JSON.parse(value))


Enter fullscreen mode Exit fullscreen mode

v-on可以使用指令在父组件中捕获此类事件



v-on:change="doSomethingWithData($event)"


Enter fullscreen mode Exit fullscreen mode

请注意,它既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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

对组件的修改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>


Enter fullscreen mode Exit fullscreen mode

同样,我们将发出一个自定义事件,然后使用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>


Enter fullscreen mode Exit fullscreen mode

注意这行代码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>


Enter fullscreen mode Exit fullscreen mode

好了,差不多就是这样了。现在,将光标悬停在空间对象上,即可显示该对象的属性列表:

放大后的三角形

您可以尝试向 GeoJSON 对象定义添加属性,看看它们在检查框中是如何显示的!

如果你愿意,也可以尝试复制粘贴像这样的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