Using MapLibre GL in Tauri

Knowing almost nothing about frontend development or GIS I wanted to be able to display something on a map on my own computer (desktop). Knowing a fair bit of Rust but not having a clue about anything else involved I decided the easiest way to do this was probably to use Tauri. What I wanted is essentially Tauri + TypeScript + Vite + MapLibre GL + OpenStreetMap tiles. And this is my attempt to write up the getting started guide I wish I would have found. Most of the things will be extremely obvious to anyone who has any experience with this, so this is mostly me showing just how little I know about this all.

The Tauri App

I started with following the Tauri Quick Start for plain HTML, CSS and JavaScript. This gives you a basic example with an index.html, main.ts and main.rs files and a demonstration of how to make calls from TypeScript to Rust. I had to make a few Javascript choices I had no clue about, but was advised to use Bun for the, err, whatever that thing is in the Javascript world. I'll call it "the npm component", they themselves call it the all-in-one toolkit. I don't think this bit matters too much. But it seems to be a Javascript packages installer and be the driver for the runtime tasks.

Vite is set up by Tauri to be the, again I don't really know what this is, development server and tool that collects all your source files and makes them deployable. This matters a little as it is where to look up documentation on what things you can do syntax-wise in the index.html file, which is the entrypoint of the application.

MapLibre GL JS

Next up is to add the MapLibre GL JS Quickstart. It's right there on the landing page, so just add that code to the index.html. The first hurdle though is that this assumes you know how to get the maplibregl module into your HTML file. Hopping over to the examples there's a more complete way of getting this to work, you need to load both the script and CSS:

<link rel='stylesheet' href='https://unpkg.com/maplibre-gl@4.1.0/dist/maplibre-gl.css' />
<script src='https://unpkg.com/maplibre-gl@4.1.0/dist/maplibre-gl.js'></script>

Adding this we get a map, nice.

Of course that's not good enough, it makes my app dependent on loading this from the internet every time. The whole point of these packages is that I can ship them with the app.

Installing Modules and using TypeScript

So next up is installing the modules and using them from TypeScript instead of the index.html file. The installing part was easy enough:

bun install maplibre-gl

This ends up putting a bunch of things in node_modules, fair enough so far. Figuring out the correct way to use this from TypeScript was a lot more difficult. In the end it looks like this in main.ts:

import maplibregl from "maplibre-gl/dist/maplibre-gl.js";
import "maplibre-gl/dist/maplibre-gl.css"
import { invoke } from "@tauri-apps/api/tauri";

new maplibregl.Map({
    container: 'map',
    style: 'https://demotiles.maplibre.org/style.json',
    center: [0, 0],
    zoom: 1
});

And the only thing remaining in index.html is the <div> tag that the typescript/javascript code fills in:

<div id="map" style='width: 1000px; height: 600px;'></div>

Figuring out those imports was a big mystery for someone with no clue about all this. I had to try various ways of figuring out how to specify the path so that Vite would make things work. Some variations, like giving it the path to the module, worked in cargo tauri dev environment, but not in the final executable created by cargo tauri build.

The difference between import maplibregl on the one hand, but import { invoke } on the other, was also a big mystery I had to learn the hard way. So much of the Javascript documentation of all these concepts assume you already know all the previous ways of doing things in Javascript. It's like you're forced to re-live the almost 30 years of evolution this whole ecosystem has gone through rather than just be told how things should work today.

Anyway, the above is how you hook up MapLibre GL into TypeScript in 2024 I think. I hope it helps another poor soul sometime.

Using the OpenStreeMap Tile Provider

So far the map shows a very basic map for demo purposes. That's not really good enough for most usecases. About every example of MapLibre shows that you need to sign up to some commercial map provider who will then serve your maps for you. And I get that hosting the maps costs money, so fair enough to some level. But I want to make a tiny app and maybe share it with a few folks and would like to be independent from a commercial provider. (You can of course self-host your map and that's maybe what I'd do for something larger, but for now I just want to play around on my own.)

Turns out OpenStreetMap has a Tile Usage Policy, it has a few requirements but you can make use of their tileserver for small projects. Only no single example of MapLibre GL shows you how to do this.

There are two types of tile servers: vector and raster. This was not a surprise to me, but all MapLibre GL's examples use vector tile servers. There is an example buried somewhere deep about using a raster source, but it does not tell you this is a super nice and easy way to get started. So here's how to show the standard OpenStreetMap map:

new maplibregl.Map({
    container: 'map',
    style: {
        'version': 8,
        'sources': {
            'osm': {
                'type': 'raster',
                'tiles': [
                    'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
                ],
                'tileSize': 256,
                'attribution': '&copy; <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> contributors'
            }
        },
        'layers': [
            {
                'id': 'osm',
                'type': 'raster',
                'source': 'osm',
                'minzoom': 0,
                'maxzoom': 22,
            }
        ]
    },
    center: [0, 0],
    zoom: 1
});

Bear in mind the attribution is important to be compliant with the Tile Usage Policy.

The Tile Usage Policy also requires you to use a custom User-Agent header when fetching tiles from them. This is mostly to tell you off when you start being too heavy a user and may have to figure out alternatives for your tileserver. Though hopefully you'd realise this in time and it would not come that far. You want to set it anyway because if you use some default value probably other users could over-use the same and you can start to be blocked.

You can do this rather easily in the tauri.conf.json file by setting the tauri.windows.userAgent field to some custom value.

The other important requirement for the Tile Usage Policy is that you make this configurable at runtime. Again so that people can change it if you get blocked without having to rebuild the app. So make sure to do that before you give your app to anyone else. That's not done yet here, but is easy enough. Of course since I only know Rust I implement this on the Rust side and send back the JSON to configure the map layer.

Now Have Fun

That's it, now you can browse the examples of the MapLibre GL JS documentation and add the things to the map which you want. Of course I produce my data entirely in Rust and use the geojson crate to get it in the right shape for the maplibregl functions. Using the TypeScript code as nothing more than a proxy to call my Rust code and pass the results to specific maplibregl functions.