Integrating an OpenLayers map in Vue.js, a step-by-step guide
Tags: vue, openlayers, geojson, camptocamp
Published: 2020-09-08 12:05:54

<small>_Cover art by [Donato Giacola](https://donatoarts.com/magic)_.</small>

# Introduction

Hi! Chances are, you have at least heard of [Vue.js](https://vuejs.org/), the very popular frontend JavaScript framework. It has a reputation of being **accessible**, **well documented** and easily **understandable**.

On the other hand, you may or may not have heard about [OpenLayers](https://www.openlayers.org), one of the oldest web mapping libraries around. That’s OK, maps can be **surprisingly complex** at times and not everyone is eager to dive into this complexity when services like the [Google Maps API](https://developers.google.com/maps/documentation/embed/get-started) make things much simpler. But keep in mind that there are **many, many more things** that a web mapping library can do for you besides showing a marker on a map!

> Note that there are other similar libraries around (see [this article](https://dev.to/camptocamp-geo/the-3-best-open-source-web-mapping-libraries-57o7) for a quick tour). We will stick with OpenLayers since it offers the most possibilities in the long run.

In this article we _will_ dive into how both Vue.js and OpenLayers work, and how to put **an interactive map in a Vue app** and make it actually useful! At the end of this article we will have built a simple geospatial object editor which will allow us to:

*   modify an object in GeoJSON format and see it appear on the map
*   edit an object geometry directly on the map

You can take a look at a running instance of the project [here](https://jahow.github.io/ol-vue-example/). Sources are available [here](https://github.com/jahow/ol-vue-example).


# Set up the application

There are already numerous tutorials on how to scaffold a Vue application out there, so we’re going to jump that part. Using the [Vue CLI](https://cli.vuejs.org/) is very straightforward anyway, and calling `vue create my-app` will get you most of the way.

From now on let’s assume we have a simple application with the main page divided into three frames:

#### `App.vue`

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

The three frames are named _Map_, _Edit_ and _Inspect_. Notice how we used [CSS Grid](https://css-tricks.com/snippets/css/complete-guide-grid/) to get the layout done? This is the result:

![3 gray frames](https://dev-to-uploads.s3.amazonaws.com/i/7rsn2t3i18xskc7fms01.png)


Nice! This is how we’ll proceed next: create the `Map` component, then the `Edit` one and finally the `Inspect` one.


# Give me the map!

Let’s create a `MapContainer` component, and include it in the main app. This is where we will need OpenLayers, so remember to install it first:

```bash
npm install --save ol
```

Then create the Vue component:

#### `MapContainer.vue`

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

This is getting interesting. See, creating a map with OpenLayers isn’t done in one line: you have to give it one or several `Layer` objects and also assign it a `View`. Have you noticed the `constrainResolution: true` option for the map view? This simply is to make sure that the map zoom snaps at the correct levels to have the OSM tiles looking crisp (see the [API doc](https://openlayers.org/en/latest/apidoc/module-ol_View-View.html) for more info).

Also notice that we kept a reference to the map root using the [`ref` Vue directive](https://vuejs.org/v2/api/#ref) like so:

```html
<div ref="map-root"
```

The [Map constructor](https://openlayers.org/en/latest/apidoc/module-ol_Map-Map.html) can either take a CSS selector or an actual HTML element, so we then just have to fetch the map root element using `this.$refs['map-root']`.

The result should look like this:

![OSM world map](https://dev-to-uploads.s3.amazonaws.com/i/5tlbr6zjn79qw10sv0ub.png)


Ok, we have a map, it’s interactive, but there’s not much more to it. Well of course it contains the whole world, but apart from that… How about we add an object on it?

#### `MapContainer.vue`

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

Simple!


![OSM map with triangle](https://dev-to-uploads.s3.amazonaws.com/i/ojmm4p9gh7orlso8j2f3.png)



# Modifying the object

The object we’re currently showing is expressed in [GeoJSON](https://geojson.org/). Good thing is, this format is easily edited by hand! Let’s create a new component for doing just that.

#### `Edit.vue`

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

This component takes a `geojson` prop as input, which will be the same as the one given to the `MapContainer` component.

OK, let's have a quick look at some Vue logic now.

The `v-model="geojsonEdit"` attribute in the component template is a [Vue directive commonly used for form inputs](https://vuejs.org/v2/guide/forms.html#Basic-Usage). It defines a *two-way data binding* with the `geojsonEdit` local property, meaning any input by the user will be saved in the property and any change to the property will be reflected on the screen.

For this component to have any effect we want to inform the parent component whenever the GeoJSON text was modified. To do that, we'll **dispatch an event**, which is done in Vue like so:

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

Such an event can be captured in a parent component using the [`v-on` directive](https://vuejs.org/v2/api/#v-on):

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

Note that `v-on` is useful for **both custom-defined events and standard HTML5 events**. Take a look at the [Vue events guide](https://vuejs.org/v2/guide/events.html) for more details.

Now, how are we going to know _when_ to emit that `change` event? The immediate response would be to set up a watcher on `geojsonEdit` and trigger an event whenever it changes.

In the code above we went with another solution: defining a [computed property](https://vuejs.org/v2/guide/computed.html). Using a computed property is very helpful in this case as it allows us to specify **two different behaviours (read and write)** without resorting to watchers. This way, we simply have to emit the event on the `set()` method, and read from the input data on the `get()` method. As an added bonus, we're **not maintaining any internal state** on the component, which is always a good thing in the long run.

Let's get back on track now. The other components cannot yet handle an update of the spatial object as it’s currently hardcoded in the `MapContainer` component.

We're going to modify both the `MapContainer` as well the `App` components to handle varying data:

#### `MapContainer.vue`

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

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

The modification to the `App` component was pretty straightforward. We’re really just storing the data on this level instead of on the `MapContainer` one, and passing it as input to the two child components.

As for `MapContainer`, the modification was a bit more involved but not by much: by watching the `geojson` input prop, we’re making sure the **OpenLayers map stays in sync with the Vue component state**.

The result should look like this:


![Zoomed in triangle](https://dev-to-uploads.s3.amazonaws.com/i/aquzd263edefuebjqfq3.png)


As an added bonus, the map view now automatically zooms into the displayed object! But the best part is that if you change the GeoJSON definition on the right… the object is modified in real time! Pretty nice right?


# Time for inspection

Spatial features often hold a series of so-called **attributes**, essentially key-value pairs. In GeoJSON you add some in the `properties` field of a feature. These are sometimes displayed on-screen (eg. labels) but often most of them are "hidden" and only shown in a tooltip or similar.

To push the exercise a bit further, let’s create a new `Inspect` component which will show all the attributes of the feature that’s currently beneath the pointer.

We’ll start by making it so that the `MapContainer` component emits a `select` event whenever a feature is found under the pointer:

#### `MapContainer.vue`

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

Again, we're emitting a custom event which will then be capture using `v-on:select="..."` on the parent component.

Also, we’re using the [`forEachFeatureAtPixel`](https://openlayers.org/en/latest/apidoc/module-ol_Map-Map.html#forEachFeatureAtPixel) method for finding out features under the cursor, which looks for _all_ features in _any_ layer at a certain pixel, and applies the given callback for each of them. In this case we only want one feature, so we exit after the first match (since the callback `(feature) => feature` returns a truthy value).

We can then proceed to create the `Inspect` component, which will simply show all the feature’s attributes:

#### `Inspect.vue`

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

Notice the line `this.feature.getKeys().filter(key => key !== this.feature.getGeometryName())`? This will return the key of all the feature’s attributes, except the one that holds the geometry (because that would render things unreadable, you’re welcome to try). See the OpenLayers [Feature API doc](https://openlayers.org/en/latest/apidoc/module-ol_Feature-Feature.html#getKeys) for more info.

Finally, let’s glue everything together in the `App` component:

#### `App.vue`

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

Well, that’s pretty much it. Now hovering your cursor over a spatial object will give you the list of properties from that object:

![Zoomed in triangle](https://dev-to-uploads.s3.amazonaws.com/i/dz1mexrdog9s8tfibgj2.png)


You can try and add attributes to the GeoJSON object definition and see how they show up in the inspect frame!

If you want to, you can also try and copy-paste a GeoJSON file like [this one](https://openlayers.org/en/latest/examples/data/geojson/countries.geojson) (containing all countries with simplified shapes). After all, we’re **not making any assumptions** on what the GeoJSON data contains!


![Countries GeoJSON](https://dev-to-uploads.s3.amazonaws.com/i/gzk6lxyn874huji0cqjt.png)


Note that there are optimizations to be made for better performance, especially making sure that the `MapContainer` component only emits an event when necessary (i.e. not emitting the same feature on every `pointermove` event).


# Conclusion

Throughout this tutorial we managed to create a simple app that allows both editing and inspecting spatial objects. This wasn’t so hard considering we only needed **three components**, each of them with **limited responsibility**, and one root component for storing the application state.

Hopefully this helped shed some light both on some **core Vue.js concepts**, as well as **OpenLayers ones**.

You can take a look at the final source code [here](https://github.com/jahow/ol-vue-example), which contains a few improvements over this tutorial.

I hope you enjoyed this tutorial, there are many other things we could do to expand on this basis, let me know if you have any ideas! Thanks for reading!