Héberger son serveur de cartes OpenStreetMap

Cet article est la seconde partie de ma série sur OpenStreetMap :

Dans mon précédent article, j'ai montré comment on pouvait afficher des cartes grâce aux données d'OpenStreetMap. Mais même si ça a l'avantage indéniable de permettre de se passer de Google, on doit toujours passer par un service externe.
Nous allons voir maintenant comment nous pouvons héberger nous-même nos cartes, afin de supprimer cette dépendance.

Installation du serveur de cartes

Il existe plusieurs de serveurs. Étant donné que je souhaite utiliser mes cartes en mode vectoriel, j'ai décidé d'utiliser un serveur OpenMapTiles PHP.

Il existe aussi une version du serveur en NodeJS (déployable avec Docker), ou si vous préférez rester sur la version la plus classique d'OpenStreetMap, vous pouvez aussi suivre cet article de Switch2OSM.

Pour déployer mon serveur, j'ai créé un App Service sur Azure, au niveau gratuit (il vous faudra certainement une machine plus puissante en prod), avec la stack PHP.

Ensuite, il faut installer le serveur PHP. Il suffit de récupérer la dernière version de tileserver-php sur Github, et de copier les 2 fichiers tileserver.php ainsi que le .htaccess dans l'App Service.

Mettez à jour le .htaccess pour configurer les en-têtes CORS - décommentez les lignes ci-dessous, et mettez à jour avec vos domaines autorisés :

# Option: CORS header for cross-domain origin access to all data
<ifModule mod_headers.c>
  Header set Access-Control-Allow-Origin *
</ifModule>

Vous pouvez maintenant copier les 2 fichiers tileserver.php et .htaccess dans l'App Service.
C'est bon votre serveur fonctionne, même s'il est encore un peu vide.

Téléchargement des données

En effet, maintenant que votre serveur tourne, il faut ajouter les données des cartes. Vous pouvez les télécharger sur le site d'OpenMapTiles.

Il s'agit d'un service payant qui fournit les données avec des mises à jour hebdomadaires, mais vous pouvez récupérer des données gratuitement pour des projets non-commerciaux/open-source, ou pour vos tests. Si vous ne souhaitez pas vous inscrire, vous pouvez télécharger directement la carte de Toulouse.

Mettez ce fichier .mbtiles dans le même dossier que tileserver.php, et cette fois le serveur est prêt à être utilisé.

Sélection d'un style

Jusque là, nous avons réussi à mettre en place le serveur, mais il va nous fournir uniquement les objects vectoriels pour afficher la carte, il va nous manquer le style, c'est à dire un fichier json qui va définir de quelle manière doivent s'afficher chaque type d'objet. Si vous vous souvenez de l'article précédent, c'est ce fichier là qui est référencé lors de l'initialisation de la carte avec la librairie Mapbox GL.

Nous n'allons pas rentrer dans les détails de la définition du style, nous allons nous pour le moment prendre l'un des styles existant proposés par OpenMapTiles. Histoire de changer un peu de ce dont on a l'habitude, j'ai choisi le style Positron, et je suis allé télécharger sa branche gh-pages sur Github.

Une fois téléchargé, il va falloir le modifier un peu avant qu'il ne soit utilisable.

Première chose, prenez le fichier style-cdn.json, que vous pouvez renommer en style.json, et cherchez les lignes suivantes :

"sources": {
  "openmaptiles": {
    "type": "vector",
    "url": "https://maps.tilehosting.com/data/v3.json?key=SymbVzXrAD6Jmqe6yBOS"
  }
},

Ici l'url correspond au serveur de tuiles. Il faut la remplacer par un lien vers l'API de notre serveur : https://maps-lacasa.azurewebsites.net/tileserver.php?/2017-07-03_france_toulouse.json (il s'agit du lien vers le fichier php, dont la requête prend en paramètre le nom du fichier de données, et avec l'extension json)

Ensuite, plus bas se trouve le lien vers les sprites (les 2 fichiers sprite.png et sprite.json qui sont fournis avec le thème) - remplacez le par le lien vers vos fichiers sprite, sans extension : https://maps-lacasa.azurewebsites.net/positron/sprite (ici j'ai déployé le style sur le même serveur pour des raisons pratiques, mais les styles peuvent être déployés ailleurs).

Dernier point à mettre à jour : les fonts utilisées pour l'écriture du texte. Cherchez toutes les occurences de "text-font" dans le style.json :

"text-font": [
  "Metropolis Medium Italic",
  "Noto Sans Italic"
],

Et modifiez les pour n'en garder qu'une à chaque fois (car selon la documentation, ce système d'hébergement statique ne gère pas les font alternatives). Vous pouvez choisir celle que vous préférez, ou en choisir une tout autre.

"text-font": [ "Metropolis Medium Italic" ],

Vous pouvez récupérer les fonts sur Github, dans un repo dédié, et les copier sur votre hébergement web.

Maintenant vous pouvez faire un lien vers ces fichiers dans le style.json, dans la propriété glyphs :

"glyphs": "https://maps.tilehosting.com/fonts/{fontstack}/{range}.pbf?key=SymbVzXrAD6Jmqe6yBOS",

Que vous pouvez remplacer par le chemin vers votre dossier de fonts, dans mon cas https://maps-lacasa.azurewebsites.net/fonts/{fontstack}/{range}.pbf. fontstack et range seront appelés dynamiquement selon le nom de la font défini dans le style, et le niveau de zoom affiché.

Et cette fois on est bon, tout est en place pour afficher notre carte sans aucun appel externe ; il suffit de remplacer dans notre javascript le lien vers le style par notre nouveau style modifié.

Afficher une carte grâce aux données OpenStreetMap

Cet article est la première partie de ma série sur OpenStreetMap :

Cette semaine est sorti le jeu Flight Simulator 2020, et certains joueurs se sont amusés de la présence d'un immense monolithe au nord de Melbourne.
Ce bug a pour origine une erreur dans le service cartographique OpenStreetMap (qui a entre-temps été corrigé), et cette anecdote m'a rappelé que ça faisait un moment que je voulais vous en parler.

OpenStreetMap, pour ceux qui ne connaissent pas, est ce qu'on pourrait appeler le « Wikipedia de la cartographie ». Il s'agit d'un service de cartographie libre, utlisable gratuitement, et pouvant être mis à jour par n'importe qui.
Vous pouvez donc tout à fait corriger la carte si vous voyez des erreurs dans votre voisinage, ou la compléter avec les noms de vos magasins préférés.

Je tiens un blog de développement web, donc je ne vais pas approfondir le sujet de la cartographie, bien que le sujet soit passionnant, mais nous allons plutôt voir comment nous pouvons utiliser les données d'OpenStreetMap pour afficher une carte sur nos applications web, à la place de Google Maps qui est un peu trop souvent utilisé sans se poser la question des alternatives.

Licence

Avant de commencer, on va voir rapidement la partie légale.

Les données brutes sont distribuées sous la licence libre Open Data Commons Open Database License (ODbL). Comme indiqué sur la page copyright d'OpenStreetMap, vous pouvez utiliser les données dans vos applications, à condition de créditer les contributeurs d'OpenStreetMap, et de redistribuer les données sous la même licence.
Lorsqu'on veut afficher la carte, on va utiliser les tuiles (les images des cartes, générées à partir des données brutes). Ces images sont sous licence Creative Commons CC-BY-SA 2.0 - les conditions d'utilisation sont globalement les mêmes que pour les données brutes : n'oubliez pas de mettre le copyright.

Affichage d'une carte avec Leaflet

Pour afficher une carte dynamique dans une page web, il va tout d'abord falloir choisir un serveur de tuile, le serveur qui nous fournira les fonds de carte.
La fondation OpenStreetMap n'a pas les moyens de financer des serveurs de tuiles utilisables librement par tout le monde, mais les données étant libres, de nombreux services proposent d'utiliser leurs serveurs - certains gratuitement pour des usages modérés, d'autres fournissant des services professionnels contre rémunération. Notez que vous pouvez aussi installer votre propre serveur de tuiles, si vous souhaitez être autonome.

Mon choix pour cet article s'est porté sur celui de l'association OpenStreetMap France.

On va maintenant utiliser une librairie JavaScript pour l'affichage. De nombreuses solutions existent, les plus utilisées sont OpenLayers et Leaflet ; j'utilise ce dernier dans cet exemple.

Pour afficher la carte, créer un div qui la contiendra, en lui précisant obligatoirement une taille.
Ensuite, vous pouvez initlialiser la carte, avec cet exemple de code :

<div id="mapDiv" style="width:800px; height: 400px;"></div>

<script>
    // Initialisation de la carte
    var map = L.map('mapDiv'); 

    // Initialisation des tuiles 
    var tileUrl = "https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png";
    var copyright = '&copy; Contributeurs de <a href="https://www.openstreetmap.org/">OpenStreetMap</a>';
    L.tileLayer(tileUrl, {
        attribution: copyright,
        maxZoom: 18,
        tileSize: 512,
        zoomOffset: -1,
    }).addTo(map);
    
    // On zoome sur Toulouse
    map.setView([43.60, 1.44], 10); 
</script>

Ce qui nous donne le résultat suivant :


Vous pouvez ensuite afficher toutes sorte d'informations sur la carte ou intéragir avec l'utilisateur ; je vous laisse jete un œil à la documentation pour ça.

Affichage vectoriel avec OpenMapTiles

Les données brutes étant libres, il est possible d'afficher les cartes autrement qu'en chargeant des images d'un serveur de tuiles. C'est ce que fait le projet OpenMapTiles, en proposant un système de tuiles vectorielles.
Au lieu d'afficher des images, c'est le navigateur qui dessine les objets de la carte ; permettant ainsi un affichage plus fluide et des plus d'interactions possibles (essayer de bouger avec le clic droit dans l'exemple plus bas). C'est le choix qu'a fait Qwant pour son service de cartographie Qwant Maps.

Ceci a malheureusement un coût, puisque le navigateur de l'utilisateur est plus fortement mis à contribution, et peut poser des problèmes de performances sur des ordinateurs peu puissants ou certains mobiles.

Pour afficher une carte vectorielle, j'utilise cette fois les serveurs de MapTiler, et la librairie Mapbox GL JS. OpenLayers et Leaflet ne gèrent pas l'affichage vectoriel nativement, mais peuvent fonctionner avec des plugins.

Pour afficher la carte, l'API est un peu différente, mais le système reste le même, je vous laisse regarder le code :

<div id="mapDiv" style="width:800px; height: 400px;"></div>

<script>
    var map = new mapboxgl.Map({
        container: 'mapDiv',
        style: 'https://api.maptiler.com/maps/bright/style.json?key=apikey',
        center: [1.44, 43.60],
        zoom: 10
    });
</script>

Vous pouvez ainsi voir le résultat - et voir la différence avec la carte précédente :

Vous noterez que même si l'affichage est différent, les cartes sont les mêmes. Les rues, les bâtiments ne changent pas de l'un à l'autre, car la source des données est la même.

J'espère que je vous ai donné envie d'essayer ça, et que la prochaine fois que vous devrez afficher une carte dans l'un de vos projets vous étudierez l'option OpenStreetMap.

Utiliser un composant Blazor dans une application ASP.NET Core MVC

Blazor est un nouveau framework qui arrive avec ASP.NET Core 3, et qui permets de faire du développement web client en C# au lieu de Javascript. Il propose 2 modèles d'hébergement : un mode client, où les dlls sont envoyées au client avec un runtime .NET, et exécutées grâce à WebAssembly ; et un mode serveur, où le code est exécuté par le serveur, et le client reçoit les mises à jour par une connexion SignalR. Pour plus d'informations, je vous invite à aller lire la documentation de Blazor.

Lorsque vous démarrez un nouveau projet ASP.NET Core3, vous avez la possibilité de créer un projet pur Blazor, ou un projet MVC, mais il est tout à fait possible d'ajouter des composants Blazor à une ou plusieurs page d'un projet MVC.

Note : cet article utilise Blazor en mode server-side, et est basé sur la preview 7 - il peut y avoir des changements dans les prochaines versions.

Migration vers ASP.NET Core 3

Blazor est une nouveauté d'ASP.NET Core 3. Si vous démarrez un nouveau projet ASP.NET Core 3, vous pouvez passer au paragraphe suivant.

Si vous souhaitez utiliser Blazor sur un projet existant sur un framework précédent, il vous faudra d'abord le migrer vers la dernière version : comme d'habitude, lisez la documentation pour effectuer la migration, notamment le passage sur la migration du code de routage pour utiliser .UseEndpoints().

Configuration de Blazor dans le Startup.cs

Pour activer Blazor dans le projet, il y a 2 modifications à faire dans le Startup.cs :

  • Dans la méthode ConfigureServices, ajoutez services.AddServerSideBlazor(); : enregistrement du service Blazor
  • Dans la méthode Configure, allez dans le UseEndoints et ajoutez endpoints.MapBlazorHub(); : configuration du hub SignalR utilisé par Blazor

On se retrouve avec le code qui ressemble à ça :

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddServerSideBlazor();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
        endpoints.MapBlazorHub();
    });
}

Ajout d'un composant Blazor dans une page

Pour mon exemple, j'ai repris le composant Counter du template Blazor, sans l'attribut @page

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    int currentCount = 0;

    void IncrementCount()
    {
        currentCount++;
    }
}

Puis on peux référencer le composant depuis notre vue MVC :

@(await Html.RenderComponentAsync<BlazorInMvc.Components.Counter>(RenderMode.Server))

Vous pouvez lancer la page : vous verrez le composant s'afficher avec le "Current count" à 0, et le bouton. Mais si vous cliquez dessus, rien ne se passe. Il faut ajouter un fichier javascript pour établir la connexion SignalR :

@section Scripts{
    <script src="~/_framework/blazor.server.js"></script>
}

Si l'url de votre page n'est pas à la racine du site, la connexion ne pourra pas s'effectuer correctement. Dans ce cas, il faut rajouter un élément dans l'en-tête de votre page pour préciser le dossier où elle se trouve. Si l'url de ma page est /Home/Counter, le dossier est /Home. J'ajoute donc à ma page :

@section Header{
    <base href="~/Home" />
}

Maintenant vous pouvez retester, et le composant fonctionne : les clics sur le bouton mettent bien à jour le compteur.

Utilisation de la navigation Blazor

À priori, l'utilisation de la navigation Blazor avec le routing MVC risque de poser soucis, mais en étant rigoureux dans la définition de ses routes, on peut faire fonctionner les 2 ensembles.

J'ai 2 pages MVC : /Home et /Home/Counter (même si l'une contient un composant Blazor, c'est toujours une page MVC), et je veux créer 2 pages Blazor : /Nav/Page1 et /Nav/Page2, qui fonctionneront en mode SPA lorsqu'on navigue de l'une à l'autre (et seulement entre ces 2).

Pour faire ça, on va créer un contrôleur qui recevra les requêtes vers les pages Blazor, et on configure le routing pour que toutes les pages sous l'url /Nav pointent vers la même Action.

NavController.cs :

public class NavController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

Startup.cs :

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "blazor",
        pattern: "Nav/{page?}/{id?}",
        defaults: new { controller = "Nav", action = "Index" }
    );

    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");

    endpoints.MapBlazorHub();
});

L'action Index va contenir un composant Blazor, qui servira de routeur pour les 2 pages :

Nav.razor :

<Router AppAssembly="typeof(Startup).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="routeData" />
    </Found>
    <NotFound>
        <h1>Page not found</h1>
    </NotFound>
</Router>

Puis les composants Blazor correspondant à nos pages :

@page "/Nav/Page1"
@using Microsoft.AspNetCore.Components.Routing

<h3>Page1</h3>

<NavLink href="/Nav/Page2">Aller à la page 2</NavLink>

et

@page "/Nav/Page2"
@using Microsoft.AspNetCore.Components.Routing

<h3>Page2</h3>

<NavLink href="/Nav/Page1">Aller à la page 1</NavLink>

Les composants commencent par l'attribut @page, qui permets de définir à quelle url le composant doit répondre.

Maintenant vous pouvez passer de la page1 à la page2 sans avoir un rechargement complet de la page, la navigation se fait par la connexion SignalR. Si vous naviguer vers ou depuis une autre page, la navigation est une navigation html classique.

Code source

Vous pouvez récupérer le code source de cet article sur ma page Github : https://github.com/glacasa/Blog-BlazorInMvc.

Pour le compiler, vous devez avoir la dernière version de Visual Studio 2019 preview, ainsi que le framework dotnet core 3 preview 9 minimum.