OpenLayers3 (2018, part 1)

Avec OpenLayers (acronyme OL3, http://openlayers.org), on monte d'un niveau dans la pyramide de productivité. Il s'agit d'une librairie permettant de fabriquer une carte en ligne par un ensemble de couches associée à des contrôles pour gérer l'interaction avec l'utilisateur. Une carte se construit avec trois ingrédients de base : du balisage HTML, des déclarations de style CSS, et du code JavaScript.

Pour la suite, le point d'entrée pour se documenter sur l'utilisation de cette API est ici (en version 3.19.1 3.20.1 pour ce tutoriel) : (docs/API, examples, code). Aussi, profitez de cette présentation OL3 partagée par les collègues du département Géomatique.

1. Navigation cartographique

A. Les éléments de base

Pour ce tutoriel, télécharger l'archive de départ oltuto18.zip du dossier https://drive.switch.ch/index.php/s/ZUfBx9Cjx8tp13r

Décompresser l'archive dans un répertoire projet dans votre serveur web PHP (local et/ou avec l'app Heroku)

Le fichier js/config.js intialise de nombreuses variables, notamment les URL de géoservices cartographiques

Certains exemples sont basés sur des géodonnées disponibles dans ce dossier (ex. cities, world_simple) : https://drive.switch.ch/index.php/s/0BjD03Uz4zF5eYW

La maîtrise des fonctionnalités Web Developer de votre navigateur est indispensable

Aussi un outil comme REST Client peut être utile

Vous pouvez télécharger la librairie OL3 en local dans le répertoire lib depuis https://github.com/openlayers/openlayers/releases/tag/v3.20.1

B. Créer une carte, un premier exemple pour comprendre

Balisage HTML et déclarations CSS

Le bloc <div> d'ID map est la cible de visualisation (la vue cartographique)

C'est la position et la taille de cette div qui déterminent comment la vue s'intègre dans l'interface

OpenLayers est accompagné de nombreuses prédéfinitions de déclarations CSS

Initialisation de la carte

L'objet ol.Map est associé à l'ID de la cible de visualisation

L'objet ol.View représente la vue 2D de la map. Il permet de définir le centre, la résolution et la rotation de la map

L'objet ol.Layer est utilisé pour définir chaque couche à ajouter à la carte

L'objet ol.source.ImageWMS permet de définir les paramètres d'une requête WMS pour obtenir une couche servie par un serveur cartographique conforme au standard OGC WMS (http://www.opengeospatial.org/standards/wms)

La méthode fit termine l'initialisation en calculant l'enveloppe géographique de visualisation

<html>
<head>
<title>ol3 - Create a first map with an OGC WMS layer</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function () {
map = new ol.Map({
target: 'map'
});
var wmsLayer = new ol.layer.Image({
source: new ol.source.ImageWMS({
url: blWMS,
ratio: 1,
params: {
VERSION: "1.0.0",
LAYERS: "ne_10m_admin_0_countries",
FORMAT: "image/png"
}
})
});
map.addLayer(wmsLayer);
// Configuration of the map view
var v0 = new ol.View({projection: "EPSG:4326"});
var extent_init = [-18, 37, 38, 57]; // or v0.getProjection().getExtent();
v0.fit(extent_init, map.getSize());
map.setView(v0);
//showExtent(extent_init);
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Remplacer la configuration de la map view comme suit - quelle différence avec précédemment ?

var v2 = new ol.View({projection: "EPSG:4326"});
var cbox = [10, 47]; // NB: c'est le centre de l'extent_init ci-dessus
v2.setCenter(cbox);
v2.setZoom(5);
map.setView(v2);

Remplacer la configuration de la map view comme suit - quelle différence avec précédemment ?

map.getView().setCenter(ol.proj.transform([10, 47], "EPSG:4326", "EPSG:3857"));
map.getView().setZoom(5);
console.log(map.getView().getProjection().getCode());

Redéfinir la couche wmsLayer comme suit - quelle différence en ajoutant une couche Tile plutôt que Image? :

var wmsLayer = new ol.layer.Tile({
source: new ol.source.TileWMS({
url: blWMS,
ratio: 1,
params: {
VERSION: "1.0.0",
LAYERS: "ne_10m_admin_0_countries",
FORMAT: "image/png"
}
})
});

Rechercher et tester d'autres couches de serveurs cartographiques WMS, comme par exemple celui de Swisstopo (voir config.js et http://spatialreference.org/ref/epsg/21781/)

Repérez les déclarations qui habillent les boutons zoomin/zoomout et les ajuster pour que ces boutons ressemblent à ça

2. Cache de tuiles

Avec une source ol.source.TileWMS c'est le client qui contrôle une grille de tuiles et il peut la garder en cache. Néanmoins, à chaque requête initiale de tuile, tout le processus de rendu cartographique se fait. Mais puisqu'une couche de tuiles fait des requêtes d'images selon une grille régulière, il est possible pour le serveur de préparer un cache de ces images.

Le service cartographique WMS permet une grande flexibilité en terme de ce que le client peut demander. Côté serveur, cela rend difficile la mise en cache. A l'extrême opposé, un service peut aussi offrir des tuiles pour un ensemble prédéfini de niveaux de zoom et pour une grille régulière prédéfinie. On parle de couches de tuiles avec une source XYZ - avec X et Y indiquant la colonne et la rangée de la grille et Z le niveau de zoom. Il faut voir cela comme une pyramide de tuiles.

A. ol.source.OSM

<html>
<head>
<title>ol3 - webmap with layer from tile server</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
]
});
map.getView().setCenter(ol.proj.transform([6.15,46.2],"EPSG:4326","EPSG:3857"));
map.getView().setZoom(10);
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Remplacer la source de la couche comme suit :

source: new ol.source.OSM({
url:"http://api.tiles.mapbox.com/v3/oertz.map-i2ak2ozc/{z}/{x}/{y}.png",
attributions: [
new ol.Attribution({
html: "Restyled & based on <a href='www.openstreetmap.org'>OpenStreetMap</a>"
})
]
})

et ajouter l'instruction CSS suivante :

.ol-attribution {
background-color: CornflowerBlue !important;
right: 10px;
left: initial;
bottom: initial;
top: 10px;
}

Remplacer la source ol.source.OSM par une source ol.source.XYZ comme suit :

source: new ol.source.XYZ({
url: "http://urbangene.heig-vd.ch/tilecache/{z}/{x}/{y}.png"
})

B. ol.source.BingMaps

L'utilisation d'un cache serveur de tuiles a largement été popularisé par des services propriétaires comme Bing Maps. OpenLayers étant indépendant de toute source de données, l'API offre également un type de couche pour dialoguer avec de tels services. La classe ol.source.BingMaps s'occupe de construire les requêtes pour interagir avec le service de tuiles cartographiques BingMaps : https://msdn.microsoft.com/en-us/library/ff701716.aspx#Anchor_0.

TODO https://www.mediamaps.ch/oltuto/Ex2b.html

Partir de l'état initial de l'exemple précédent en adaptant le code JavaScript comme suit, exécuter et analyser la requête générée vers le serveur de tuile BingMaps :

map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.BingMaps({
key: 'AqE05oJsq-bWa50FPOW2S0eQm9Oqqygc1VTi_WPhUIoKR_-jgA559CRbfndgWAIz',
imagerySet: 'Aerial'
})
})
]
});
map.getView().setCenter(ol.proj.transform([6.15,46.2],"EPSG:4326","EPSG:3857"));
map.getView().setZoom(10);

BingMaps offre plusieurs couches carto. Modifier le paramètre de type de couche (imagerySet) selon la doc Bing Maps. Contrairement à un service WMS, le service de Bing Maps ne sait pas s'auto-décrire. Mais comme il n'y a qu'un seul service Bing Maps au monde, une documentation suffit.

Some imagerySet values:
- AerialWithLabelsOnDemand
- RoadOnDemand
- CanvasGray
- OrdnanceSurvey (notice: visible only for the London area)
- ...

Quid d'une source pour afficher les tuiles Google Maps ? Une telle source peut se configurer avec une instance ol.source.XYZ comme suit, utilisant l'habituel triplet (x,y,z) de pyramide de tuiles :

source: new ol.source.XYZ({
url: "http://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}"
})

... mais pas de ol.source.GoogleMaps ? La réponse ici https://github.com/openlayers/ol3/issues/2205 et ici https://gist.github.com/elemoine/e82c7dd4b1d0ef45a9a4.

C. ol.source.WMTS

Swisstopo offre un service de tuiles basé sur le standard WMTS et que l'on peut exploiter comme suit.

<html>
<head>
<title>ol3 - webmap with Swisstopo WMTS</title>
<script type="text/javascript" src="js/config.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/proj4js/2.2.1/proj4.js"></script>
<script src="http://epsg.io/21781.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function () {
var projection = ol.proj.get('EPSG:21781');
projection.setExtent([485869.5728, 76443.1884, 837076.5648, 299941.7864]);
map = new ol.Map({
target: 'map',
view: new ol.View({
projection: ol.proj.get("EPSG:21781"),
center: [600000, 200000],
zoom: 6
})
});
var tileGrid = new ol.tilegrid.WMTS({
origin: [420000, 350000],
resolutions: [4000, 3750, 3500, 3250, 3000, 2750, 2500, 2250, 2000, 1750, 1500, 1250, 1000, 750, 650, 500, 250, 100, 50, 20, 10, 5, 2.5, 2, 1.5, 1, 0.5],
matrixIds: [0, 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]
});
var layer = new ol.layer.Tile({
source: new ol.source.WMTS(({
url: "http://wmts.geo.admin.ch/1.0.0/{Layer}/default/20140520/21781/{TileMatrix}/{TileRow}/{TileCol}.jpeg",
tileGrid: tileGrid,
layer: "ch.swisstopo.pixelkarte-farbe",
requestEncoding: 'REST'
}))
});
map.addLayer(layer);
console.log(map.getView().getProjection().getCode());
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO https://www.mediamaps.ch/oltuto/Ex2c.html

Ajouter l'instruction suivante :

navigator.geolocation.getCurrentPosition(locationFixed, locationError);

ainsi que les deux fonctions callback qui vont avec :

function locationFixed(position) {
map.getView().setCenter(ol.proj.transform([position.coords.longitude, position.coords.latitude],"EPSG:4326","EPSG:21781"));
}
function locationError(e) {
console.log(e);
}

Intéressant pour aider à démystifier le fonctionnement et l'utilisation de l'HTML5 Geolocation API :

Géolocalisation en HTML5 -- màj 2017

HTML5 Geolocation API – how accurate is it, really? -- un peu ancien (2012/2013)

MMA Demystifying Location Data Accuracy -- ~2015

MMA Location Terminology Guide -- ~2015

Souriez, vous êtes géolocalisés ! -- NG 2016

Utilisation d'un service de géolocalisation :

Open, crowdsourced : Mozilla Location Service (MLS)

Avec la Google Maps Geolocation API, dans un client type RESTClient

configurer une requête POST -> https://www.googleapis.com/geolocation/v1/geolocate?key=AIzaSyBpYS569CJN6A7wbDUXokxOJAbQhBokDkc

lancer la requête avec le corps suivant - quel est ce lieu ?

{
"considerIp": "false",
"wifiAccessPoints": [
{
"macAddress": "00:25:9c:cf:1c:ac"
},
{
"macAddress": "00:25:9c:cf:1c:ad"
}
]
}

3. Composition de couches

A. Composer "server-side"

Dans la section précédente, la composition de couches est pilotée “une fois pour toute” côté serveur. Ci-dessous un pilotage de la composition “à la demande” côté client (et donc plus il y a de couches disponibles sur le serveur cartographique, plus il y a de combinaisons possibles).

<html>
<head>
<title>ol3 - one WMS request for two layers</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Image({
source: new ol.source.ImageWMS({
url: mcWMS,
params:{
VERSION: "1.1.1",
LAYERS: 'public.world_simple,public.cities',
FORMAT: 'image/png'
}
}),
})
]
});
// default view is used here, that is with the Mercator EPSG:3857 projection
map.getView().setCenter(ol.proj.transform([7, 47],"EPSG:4326","EPSG:3857"))
map.getView().setZoom(5);
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Vérifier que la composition est bien faite côté serveur.

Quel intérêt à utiliser cette technique ? Quels inconvénients ? Utilise-t-on tout le potentiel d'un client navigateur pour faciliter la composition “à la demande” par l'utilisateur ?

B. Composer "client-side"

Une application de cartographie en ligne se doit de permettre à l'utilisateur de personnaliser sa carte par ses choix pour la couche de référence (baselayer) et les couches à activer en superposition (overlays). Elle doit fournir la capacité de gérer la composition côté client.

<html>
<head>
<title>ol3 - two WMS overlayed layers</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function () {
map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Image({
title: 'World simple',
source: new ol.source.ImageWMS({
url: mcWMS,
params: {
VERSION: "1.1.1",
LAYERS: 'public.world_simple',
FORMAT: 'image/png'
}
})
}),
new ol.layer.Image({
title: 'Cities',
source: new ol.source.ImageWMS({
url: mcWMS,
params: {
VERSION: "1.1.1",
LAYERS: 'public.cities',
FORMAT: 'image/png'
}
})
})
]
});
map.getView().setCenter(ol.proj.transform([7, 47], "EPSG:4326", "EPSG:3857"))
map.getView().setZoom(5);
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO https://www.mediamaps.ch/oltuto/Ex3b.html

Apporter la preuve que l'assemblage s'est fait côté client.

Analyser les paramètres WMS. Lequel représente l'indispensable capacité “server-side” pour permettre l'assemblage client-side par superposition ?

Ajouter dans la table des couches les ne_10m_admin_0_countries et ne_10m_lakes du GeoServer de Boundless (blWMS) comme suit :

new ol.layer.Image({
title: "Countries",
source: new ol.source.ImageWMS({
url: blWMS,
params: {
VERSION: "1.0.0",
LAYERS: "ne_10m_admin_0_countries",
FORMAT: "image/png"
}
})
}),
new ol.layer.Image({
title: "Lakes",
source: new ol.source.ImageWMS({
url: blWMS,
params: {
VERSION: "1.0.0",
LAYERS: "ne_10m_lakes",
FORMAT: "image/png"
}
})
})

Visualiser le résultat. Qu'est-ce qui est “baselayer”, qu'est-ce qui est “overlays” ?

Remettre un peu d'ordre là où c'est possible.

C. Gestion de composition cartographique - LayerSwitcher

OL3 n'offre pour l'instant pas nativement de “UI manager” de composition cartographique. Néanmoins l'API offre ce qu'il faut pour concevoir son propre LayerSwitcher.

<html>
<head>
<title>ol3 - MyLayerSwitcher</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map, lyrs;
$(document).ready(function () {
map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Image({
source: new ol.source.ImageWMS({
url: blWMS,
params: {
VERSION: "1.0.0",
LAYERS: "ne_10m_admin_0_countries",
FORMAT: "image/png"
}
}),
}),
new ol.layer.Image({
source: new ol.source.ImageWMS({
url: mcWMS,
params: {
VERSION: "1.1.1",
LAYERS: 'public.world_simple',
FORMAT: 'image/png'
}
})
}),
new ol.layer.Image({
source: new ol.source.ImageWMS({
url: blWMS,
params: {
VERSION: "1.0.0",
LAYERS: "ne_10m_lakes",
FORMAT: "image/png"
}
}),
}),
new ol.layer.Image({
source: new ol.source.ImageWMS({
url: mcWMS,
params: {
VERSION: "1.1.1",
LAYERS: 'public.cities',
FORMAT: 'image/png'
}
})
})
]
});
map.getView().setCenter(ol.proj.transform([7, 47], "EPSG:4326", "EPSG:3857"))
map.getView().setZoom(5);
initMyLayerSwitcher();
});
/***
* initMyLayerSwitcher: a first attempt to manage baselayers and overlayers.
*
* Remark: in the current state, the main drawback is that it is absolutly
* not flexible and adaptative!
*
* @returns {undefined}
*/
function initMyLayerSwitcher(){
lyrs = map.getLayers().getArray();
// Set visibility of the baselayer
lyrs[0].setVisible(true);
lyrs[1].setVisible(false);
// Set visibility of the overlays
lyrs[2].setVisible(false);
lyrs[3].setVisible(false);
$('#overlay option').prop('selected', false);
// Define the callback when the user does change the baselayer
$("#base").change(function (e) {
// Reset all baselayers
lyrs[0].setVisible(false);
lyrs[1].setVisible(false);
// Activate the one that is selected (based on index)
var idxVisible = $(this).find(":selected").index();
lyrs[idxVisible].setVisible(true);
});
// Define the callback when the user does change the overlays
$("#overlay").change(function (e) {
$(this).find("option").each(function (i, e) {
// i + 2 because the 1st and 2nd elements in the table of layers are the baselayers
lyrs[i + 2].setVisible(e.selected);
});
});
}
</script>
<style type="text/css">
#map {
width: 80%;
height: 100%;
}
#switcher {
position: absolute;
left: 82%;
top: 2%;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="switcher">
<p>Choose baselayer:</p>
<select id="base">
<option value="base1">Countries</option>
<option value="base2">World simple</option>
</select>
<p>Choose overlays:</p>
<select id="overlay" size="2" multiple>
<option value="overlay1">Lakes</option>
<option value="overlay2">Cities</option>
</select>
</div>
</body>
</html>

TODO https://www.mediamaps.ch/oltuto/Ex3c.html

Analyser le code ci-dessus et identifier la principale capacité “client-side” de l'API sur laquelle repose ce LayerSwitcher.

Adapter le code pour ajouter à l'équation la couche OSM suivante :

new ol.layer.Tile({
source: new ol.source.OSM()
})

Ajouter la propriété ci-dessous à la couche des lacs

opacity: 0.5

D. Un contrôle LayerSwitcher plus souple

A partir des capacités offertes par l'API, la communauté de développeurs gravitant autour du core OL3 développe des controls parmi lesquels un LayerSwitcher un peu plus malin. Merci @walkermatt https://github.com/walkermatt/ol3-layerswitcher.

<html>
<head>
<title>ol3 - A clever LayerSwitcher</title>
<script type="text/javascript" src="js/config.js"></script>
<link rel="stylesheet" href="lib/layerswitcher/ol3-layerswitcher.css" type="text/css">
<script src="lib/layerswitcher/ol3-layerswitcher.js" type="text/javascript"></script>
<script type="text/javascript">
var map, lyrs;
$(document).ready(function () {
map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Image({
title: "Countries",
type: "base",
source: new ol.source.ImageWMS({
url: blWMS,
params: {
VERSION: "1.0.0",
LAYERS: "ne_10m_admin_0_countries",
FORMAT: "image/png"
}
}),
}),
new ol.layer.Image({
title: 'World simple',
type: "base",
source: new ol.source.ImageWMS({
url: mcWMS,
params: {
VERSION: "1.1.1",
LAYERS: 'public.world_simple',
FORMAT: 'image/png'
}
})
}),
new ol.layer.Image({
title: "Lakes",
source: new ol.source.ImageWMS({
url: blWMS,
params: {
VERSION: "1.0.0",
LAYERS: "ne_10m_lakes",
FORMAT: "image/png"
}
}),
}),
new ol.layer.Image({
title: 'Cities',
source: new ol.source.ImageWMS({
url: mcWMS,
params: {
VERSION: "1.1.1",
LAYERS: 'public.cities',
FORMAT: 'image/png'
}
})
})
]
});
map.getView().setCenter(ol.proj.transform([7, 47], "EPSG:4326", "EPSG:3857"))
map.getView().setZoom(5);
var layerSwitcher = new ol.control.LayerSwitcher();
map.addControl(layerSwitcher);
});
</script>
<style type="text/css">
#map {
width: 80%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO https://www.mediamaps.ch/oltuto/Ex3d.html

Comment fonctionne ce ol3-layerswitcher ? (remarquez quelques nouvelles propriétés intéressantes sur ol.layer).

Ajouter une baselayer sur un base Bing Maps comme vu à l'Ex2B (ex. Aerial) - attention de définir la propriété title pour voir la couche dans le sélecteur de couches, le “layerswitcher”.

Ajouter un overlay basé sur la couche ch.swisstopo.pixelkarte-pk25.metadata-kartenblatt servie par le serveur WMS http://wms.geo.admin.ch de Swisstopo (chWMS, voir aussi la doc GeoAdmin)
Illustration du résultat attendu :