The Joy of using Leaflet/Openlayers with Vue.js

# Introduction

Webapps are becoming more and more complex, and the user interface requirements towards reactivity and snappiness are becoming more and more state of the art. In such an environment, it is helpful or even necessary to find helpers for all kind of things, to be able to prototype more quickly, to make code more modular and scalable, and better maintainable. One of these helpers are the modern JavaScript frameworks.

# Why use modern JavaScript frameworks?

Modern JavaScript frameworks like Angular, React or Vue.js are hot nowadays! You have to look hard to find front-end dev job descriptions that don’t mention them, and there are cult-like fandoms that argue about which JavaScript framework religion one does belong to. But on the same time, if you’re a beginner doing your first steps, it might seem like a complex and time-consuming task to understand what this bunch of dependencies, code structures and strange syntax really does - and why you even need it!

What comes to mind when you think about a JavaScript framework? Well, it helps with structure and reduces development time, helps maintaining your code by splitting it up in modular components and providing rendering templates, speeds up your workflow by providing powerful third party libraries and is generally the go-to method to write single page applications. These facts are all nice and handy, but they are missing the crucial benefit a modern JavaScript framework provides:

Keeping the User Interface in Sync with the State!

In the current day and age, users expect more and more fluid, easy to use and feature-packed user interfaces. You have elements of the page reacting to changes that are triggered by user input all the way at the other end of the page, buttons changing text while loading, hint texts providing feedback to users, automatically self-updating items and net sums in checkout flows, lists dynamically filling after API requests, drag-and-drop-and-move-and-delete-and-change… If you have some experience with such workflows, this may sound like a nightmare to you - keeping track of the first one or two UI changes is pretty easy, but afterwards, it becomes messy. It will inevitably happen that the user, after clicking multiple things, eventually ends up with an interface that displays something completely different than the code running in the browser itself “thinks” it is displaying. Let’s illustrate this with an example:

Let’s say we have an input field where the user can add a to-do item, which then gets added to a to-do list. Here is approximately how the (simplified) adding function would look like in HTML + Vanilla JavaScript:

<ul id=”todoList”></ul>

<script>
[...]
addTodo(title) {
    // this is the “real” logic to be used internally
    const ts = String(Date.now())
    this.state.push({ ts, title, checked: false })

    // the following is needed to update the UI
    const li = document.createElement(‘li’)
    const checkbox = document.createElement(‘input’)
    checkbox.type = ‘checkbox’
    const span = document.createElement(‘span’)
    const del = document.createElement(‘a’)
    span.innerText = title
    del.innerText =delete’
    del.setAttribute(‘data-delete-ts’, ts)

    const ul = document.getElementById(‘todoList’)
    ul.appendChild(li)
    li.appendChild(checkbox)
    li.appendChild(span)
    li.appendChild(del)
    this.items[ts] = li
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

This is far from complete, we didn’t even get started with event listeners and preparing classes to apply styling to these elements! The problem with this approach is not only that it is painful to write and read, but it is also extremely fragile. Especially when you add interaction with a server, things get out of hand real fast, and with any minimal mistake, the UI will be out of sync from the actual data! This means missing information, showing wrong information, or completely messed up with elements not responding to the user or even worse, triggering wrong actions (e.g. clicking on a delete button deletes the wrong item).

But what about jQuery, you might ask? jQuery is still used a lot nowadays for DOM manipulation, but it really excels at one thing: querying the DOM and addressing its objects. But since it is so powerful and quick to use, it doesn’t solve the actual mentioned problem, it just helps you achieve a big mess faster than with Vanilla JavaScript…

Modern frameworks to the rescue! Let’s look at how the same code example would look like in Vue.js:

<template>
<ul>
    <li v-for”todo in todos” :key=”todo.ts”>
        <input type=”checkbox” v-model=”todo.checked”/>
        <span>{{ todo.title }} </span>
        <a @click=”deleteTodo(todo)”>delete</a>
    </li>
</ul>
</template>

<script>
[...]
export default {
data: () => ({
    todos: []
      }),
    methods: {
        addTodo(title) {
            this.todos.push({
                checked: false,
                title,
                ts: String(Date.now()),
            })
        }
    },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Nice! What has happened here? We have created the to-do list structure with a HTML template that uses dynamic parts like {{ todo.title }}, and where we loop through all the to-dos in the todo array and fill the dynamic fields accordingly.

But there is something missing, you might ask! After adding a new to-do, how do I update the UI? Well, my friend, you don’t have to. Vue.js is here for you. Just push to the array, and the UI automagically updates! Other way round works too: just check the checkbox, the todo item in the array will update as well. It’s almost too good to be true!

By the way, this powerful simplicity is one of the reasons we here at EOX are leaning towards Vue.js instead of other JavaScript frameworks. Although Angular (by Google, the oldest of the bunch) and React (by Facebook, the most used) still have far more users than Vue.js, the extremely motivated open source community behind Vue, the small bundle size, the ease of use and the clean syntax gives it an edge over the competitors. The use of Vue by respectable companies such as Alibaba and Gitlab and their positive experience gives us even more reasons to do so (see Gitlabs posts on why they choose Vue.js (opens new window) over the other frameworks, and their evaluation one year later (opens new window)). Plus, we can look forward to Vue 3, which brings even better performance and even more features!

# Map libraries and Vue.js

So, how does these concepts translate to the EO world and the usage of interactive maps? For the following examples I am using OpenLayers, since it brings powerful features, especially for raster data, but the same concepts apply to Leaflet. You can use both mapping libraries in a “vanilla” way, but both have (to various degrees complete) wrappers that use the power of Vue.js from the start. You can check out VueLayers (opens new window) (used in the example below), vue-openlayers (opens new window) or wegue (opens new window) for OpenLayers wrappers, or Vue Leaflet (opens new window) and others for Leaflet wrappers.

Let’s see how a basic map setup looks like, including an OpenStreetMap (OSM) tile layer:

<template>
  <vl-map :load-tiles-while-animating="true" :load-tiles-while-interacting="true" style="height: 400px">
      <vl-view :zoom.sync="zoom" :center.sync="center" :rotation.sync="rotation"></vl-view>

      <vl-layer-tile>
          <vl-source-osm></vl-source-osm>
      </vl-layer-tile>
  </vl-map>
</template>

<script>
export default {
    data () => ({
        zoom: 2,
        center: [0, 0],
        rotation: 0,
    }),
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Pretty sweet! What’s cool about this: same as the to-do example above, the UI (in this case the map) stays in sync with the state thanks to Vue.js. So if you were to programmatically change the zoom value in the data function, the map would instantly react. The same goes the other way round, when you zoom inside the map, the zoom value in the data function will update with the new zoom value accordingly. It’s really cool to “always know what’s going on” in the UI!

Let’s see how looping through layers can look like within a template. For this example, we want to create a tile layer for each configuration in the “baseLayer” array, and for the overlay layers (in the “overlayLayers” array), we want to configure the sources to display our EOX layers (Sentinel-2 cloudless 2019, Terrain Light, Overlay Bright), we want the visibility to be handled programmatically, and we are mixing WMTS sources and XYZ sources, so the layer should be rendered accordingly. This would already be a quite complex task with vanilla OpenLayers:

<vl-layer-tile
      v-for="layer in baseLayers"
      :key="layer.layer"
      :visible="layer.visible"
      :zIndex="1"
    >
      <vl-source-wmts
        :attributions="layer.attribution"
        :url="layer.url"
        :layer-name="layer.layer"
        :matrix-set="layer.matrixSet"
        :format="layer.format"
        :style-name="layer.style"
        :projection="layer.projection"
        :resolutions="layer.resolutions"></vl-source-wmts>

    </vl-layer-tile>
    <vl-layer-tile
      v-for="layer in overlayLayers"
      :key="layer.layer"
      :visible="layer.visible"
      :zIndex="2"
    >
      <vl-source-xyz
        v-if="layer.xyz"
        :attributions="layer.attribution"
        :url="xyzUrl"></vl-source-xyz>
      <vl-source-wmts
        v-else
        :attributions="layer.attribution"
        :url="layer.url"
        :layer-name="layer.layer"
        :matrix-set="layer.matrixSet"
        :format="layer.format"
        :style-name="layer.style"
        :projection="layer.projection"
        :resolutions="layer.resolutions"></vl-source-wmts>
    </vl-layer-tile>

    <script>

const eoxMaps = {
  resolutions: [
    0.703125, 0.3515625, 0.17578125, 0.087890625, 0.0439453125, 0.02197265625,
    0.010986328125, 0.0054931640625, 0.00274658203125, 0.001373291015625,
    0.0006866455078125, 0.00034332275390625, 0.000171661376953125, 0.0000858306884765625,
  ],
  url: 'https://s2maps-tiles.eu/wmts?',
  matrixSet: 'WGS84',
  format: 'image/jpeg',
  style: 'default',
  projection: 'EPSG:4326',
};
export default {
  data: () => ({
    zoom: 5,
    center: [13, 43],
    rotation: 0,

    selectedFeatures: [],

    baseLayers: [
      {
        ...eoxMaps,
        title: '2019',
        layer: 's2cloudless-2019',
        dark: false,
        attribution: '<a class="a-light" xmlns:dct="http://purl.org/dc/terms/" href="https://s2maps.eu" property="dct:title">Sentinel-2 cloudless - https://s2maps.eu</a> by <a class="a-light" xmlns:cc="http://creativecommons.org/ns#" href="https://eox.at" property="cc:attributionName" rel="cc:attributionURL">EOX IT Services GmbH</a> (Contains modified Copernicus Sentinel data 2019)',
        visible: true,
      },
      {
        ...eoxMaps,
        title: 'Terrain light',
        layer: 'terrain-light',
        dark: true,
        attribution: 'Data &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors and <a href="https://maps.eox.at/#data">others</a>, Rendering &copy; <a href="http://eox.at">EOX</a>',
        visible: false,
      },
    ],
    overlayLayers: [
      {
        xyz: true,
        attribution: '<a href="https://scihub.copernicus.eu/twiki/pub/SciHubWebPortal/TermsConditions/TC_Sentinel_Data_31072014.pdf">Sentinel data</a>, <a href="http://maps.s5p-pal.com/">S5P-PAL</a>',
        visible: false,
      },
      {
        ...eoxMaps,
        layer: 'overlay_base_bright',
        format: 'image/png',
        attribution: 'Overlay: Data &copy; <a class="a-light" href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Made with Natural Earth, Rendering &copy; <a class="a-light" href="https://eox.at">EOX</a>',
        visible: true,
      },
    ],
  }),
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

Awesome! Let’s have a look how rendering of vectors from GeoJSON features and interactions would look like. As we have stated, with Vanilla JavaScript it’s always a pain to track user interaction and keep it in sync with the state. Here, it’s as simple as in the to-do example above, by syncing the UI with an array in the data function:

<vl-layer-vector ref="featuresLayer"
      :zIndex="3"
    >
      <vl-source-vector :features="getFeatures"></vl-source-vector>
      <vl-style-box>
        <vl-style-circle :radius="10">
          <vl-style-fill color="#004170"></vl-style-fill>
          <vl-style-stroke color="red"></vl-style-stroke>
        </vl-style-circle>
      </vl-style-box>
    </vl-layer-vector>

    <vl-interaction-select
      :features.sync="selectedFeatures"
    >
    </vl-interaction-select>

[...]

data: () => ({
    zoom: 5,
    center: [13, 43],
    rotation: 0,
    selectedFeatures: [],
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Sweet! We have created a feature vector layer that syncs the features from our GeoJSON loaded by the computed property “getFeatures” (as soon as a feature changes, it is synced to the map), and we added some simple styling of the features. Then, we have an interaction layer that registers user interactions with the map features and syncs the “select” interaction with the selectedFeatures array, meaning, every time the user selects a feature on the map it is instantly pushed to the array, and if the user deselects it, it is removed. Of course it also works the other way around, if you add a feature to the array, the user instantly sees it as selected on the map.

# Conclusion

We could go on with many more details, but the best way to explore all the different benefits of using a mapping library like OpenLayers or Leaflet with a modern JavaScript framework like Vue.js is to dig through the countless code examples and demo pages of the wrappers:

https://vuelayers.github.io/#/docs/demo (opens new window)

https://vue2-leaflet.netlify.app/examples/ (opens new window)

A recent example implementation of the concepts discussed in this article is eodash (opens new window), EOX's dashboard solution powering the Rapid Action on Coronavirus and EO (opens new window) Dashboard for ESA and the European Commission, and the Earth Observing Dashboard (opens new window) for ESA, NASA and JAXA. Vue Leaflet is used with various plugins for all its maps, which allowed us to bootstrap and publish this application within an extremely short time span. Read more about the dashboard(s) in our other blog post.

Did I convince you to try out a modern JavaScript framework for your next web app?


References: