Workshop OpenLayers v3

Dans OpenLayers, une carte est 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 d'initialisation.

1. Les bases

Pour ce workshop, télécharger le matériel de départ ol2014-workshop.zip

Décompresser l'archive dans un répertoire projet dans votre serveur web en reconfigurant le fichier js/config.js comme suit :

var myWMS = "https://eu1.mapcentia.com/wms/oertz/public";
var myWFS = "https://eu1.mapcentia.com/wfs/oertz/public/4326";

Il faut alors utiliser ce proxy_2.php qui fonctionne aussi avec des requêtes HTTPS et qui est à utiliser dans js/config.js.

A noter pour la suite, ceci qui pourra être très utile : OpenLayers API docs, OpenLayers examples

Shapefiles : cities.zip, world.zip

Ex1A : créer une carte, un premier exemple pour comprendre

<html>
<head>
<title>ol3 -Ex1A - create a first map with a WMS layer</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
$(document).ready(function(){
var map = new ol.Map({
view: new ol.View({
projection: "EPSG:4326"
}),
target: 'map'
});
var wmsImageLayer = new ol.layer.Image({
source: new ol.source.ImageWMS({
url: "https://eu1.mapcentia.com/wms/oertz/public",
params:{
VERSION: "1.1.1",
LAYERS: 'public.world_simple',
FORMAT: 'image/png'
}
}),
})
map.addLayer(wmsLayer)
// get the extent for the entire view
var extent = map.getView().getProjection().getExtent()
//fit to max extent
map.getView().fitExtent(extent, map.getSize());
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

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

TODO

Où sont-elles décrites ?

Trouvez celles qui habillent les boutons zoomin/zoomout !

Ajoutez la CSS ci-dessous

.ol-zoom .ol-zoom-in, .ol-zoom .ol-zoom-out {
background-color: tomato;
border-radius: 50%;
}

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

Un objet ol.Layer est défini pour 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 fitExtent termine l'initialisation en définissant l'enveloppe géographique de visualisation

TODO

Modifier la méthode fitExtent() comme suit (voir fitExtent)

map.getView().fitExtent([-18,38,38,55],map.getSize());
console.log(map.getView().getProjection());

Quelle différence en ajoutant une couche Tile plutôt que Image?

var wmsTileLayer = new ol.layer.Tile({
source: new ol.source.TileWMS({
url: "https://eu1.mapcentia.com/wms/oertz/public",
params:{
VERSION: "1.1.1",
LAYERS: 'public.world_simple',
FORMAT: 'image/png'
}
}),
})

Rendre visible la grille régulière des tuiles en plaçant l'instruction CSS ci-dessous au bon endroit

border: 1px solid coral;

Rechercher et tester d'autres couches de serveurs cartographiques WMS (par exemple de celui-ci : http://maps.opengeo.org/geowebcache/service/wms?request=GetCapabilities)

2. Cache de tuiles

Avec singleTile à false, le client contrôle la grille de tuiles qu'il peut 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 permettant 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.

Ex2A : ol.source.OSM

<html>
<head>
<title>ol3 -Ex2A - webmap with tiled 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',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
]
});
var extent = map.getView().getProjection().getExtent()
map.getView().fitExtent(extent, map.getSize());
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Modifier le centrage et le zoom de la carte comme suit :

//map.getView().fitExtent(extent, map.getSize());
map.getView().setCenter(ol.proj.transform([6.15,46.2],"EPSG:4326","EPSG:3857"))
map.getView().setZoom(10)

Insérer en fin de code du callback ready les logs ci-dessous :

console.log("Map projection: " + map.getView().getProjection().getCode());
console.log("Map extent: " + map.getView().calculateExtent(map.getSize()));
console.log("LonLat Mercator: " + new ol.proj.transform([6.15,46.2],"EPSG:4326","EPSG:3857"));
console.log("LonLat Google: " + new ol.proj.transform([6.15,46.2],"EPSG:4326","EPSG:3857"));

Remplacer la définition de la couche ol.source.OSM comme suit :

ol.source.OSM({
url: "http://api.tiles.mapbox.com/v3/oertz.map-i2ak2ozc/{z}/{x}/{y}.png"
})

Remplacer la définition de la couche comme suit :

ol.source.
url:"http://urbangene.heig-vd.ch/tilecache/{z}/{x}/{y}.
attributions: [
new ol.Attribution({
html: "Based on <a href='www.openstreetmap.org'>OpenStreetMap</a> / MapBox restyled by oertz :: HEIG-VD"
})
]
})

et ajouter l'instruction CSS suivante :

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

Ex2B : 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.

TODO

Partir de l'exemple précédent en adaptant le code JavaScript comme suit :

map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.BingMaps({
key: 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3',
imagerySet: 'AerialWithLabels'
})
})
]
});
map.getView().setCenter(ol.proj.transform([6.15,46.2],"EPSG:4326","EPSG:3857"))
map.getView().setZoom(6)

Modifier le paramètre de type de couche selon la documentation Bing Maps. Contrairement à un service WMS, le service de Bing Maps se sait pas s'auto-décrire. Mais comme il n'y a qu'un seul service Bing Maps au monde, une documentation suffit.

Ex2C : OpenLayers.Layer.WMTS

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

<html>
<html>
<head>
<title>ol3 -Ex2C - webmap with tiled layer</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript" src="js/swissProjection.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
var projection = new ol.proj.Projection({
code: 'EPSG:21781',
// The extent is used to determine zoom level 0. Recommended values for a
// projection's validity extent can be found at http://epsg.io/.
extent: [485869.5728, 76443.1884, 837076.5648, 299941.7864],
units: 'm'
});
ol.proj.addProjection(projection);
var 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
];
var tileGrid = new ol.tilegrid.WMTS({
origin: [420000, 350000],
resolutions: resolutions,
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]
});
var wmtsSource = new ol.source.WMTS({
tileGrid: tileGrid,
url: 'http://wmts.geo.admin.ch/1.0.0/{Layer}/default/20120809/21781/{TileMatrix}/{TileRow}/{TileCol}.jpeg', // template http://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/{Time}/21781/{TileMatrix}/{TileRow}/{TileCol}.jpeg
layer: 'ch.swisstopo.pixelkarte-farbe-pk1000.noscale',
requestEncoding: 'REST'
})
var layer = new ol.layer.Tile({
source: wmtsSource,
extent: [420000, 30000, 900000, 350000]
});
map = new ol.Map({
target: 'map',
view: new ol.View({
projection: projection,
center: [646000, 249500],
zoom: 8
})
});
map.addLayer(layer)
var extent = map.getView().getProjection().getExtent()
map.getView().fitExtent(extent, map.getSize());
})
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Par défaut, openlayers ne reconnaît pas la projection suisse EPSG:21781. Il est obligatoire de définir comment openlayers (ol.proj) doit traiter et transformer les données dans ce système de projection. Swisstopo propose un script JS (swissProjection.js) permettant de transformer des coordonées WGS84 en coordonées suisses LV03 (CH1903).

ol.proj.addCoordinateTransforms('EPSG:4326', projection, //projection: la projection définie précédemment
function(coordinate) {
return [
WGStoCHy(coordinate[1], coordinate[0]),
WGStoCHx(coordinate[1], coordinate[0])
];
},
function(coordinate) {
return [
CHtoWGSlng(coordinate[0], coordinate[1]),
CHtoWGSlat(coordinate[0], coordinate[1])
];
});

On peut ensuite centrer la carte comme suit:

map.getView().setCenter(ol.proj.transform([7.658, 45.98], 'EPSG:4326', 'EPSG:21781'));

Modifier maintenant le centrage comme suit:

map.getView().setCenter([600000, 200000]); map.getView().setZoom(8);

Modifier les paramètres de l'url comme suit:

identifier: ch.swisstopo.hiks-dufour

format: png

time: 18650101

3. Style de couche "image"

Pour le moment, c'est le serveur qui fait tourner son moteur cartographique pour créer une représentation image des données géographiques et ceci sur la base de styles prédéfinis. Voyons comment le client peut dire son mot et piloter le style “à distance”.

Ex3A : Styling avec WMS

Le standard WMS offre beaucoup de libertés, parfois au détriment de la performance. Comme il permet de demander ce que l'on veut en termes d'enveloppe géographique (paramètre bbox) et de style parmis un ensemble de styles pré-configurer, il est possible d'envoyer au serveur cartographique des instructions personnalisées de style.

<html>
<head>
<title>ol3 - Ex3A - use of a custom SLD style</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
map = new ol.Map({
view: new ol.View({
projection: "EPSG:4326"
}),
target: 'map'
});
layer = new ol.layer.Image({
source: new ol.source.ImageWMS({
url: "https://eu1.mapcentia.com/wms/oertz/public",
params:{
VERSION: "1.1.1",
LAYERS: 'public.world_simple',
FORMAT: 'image/png',
SLD: 'http://ogo.heig-vd.ch/ol2014/sld/pinkWorld.sld.xml'
}
}),
})
map.addLayer(layer)
var extent = map.getView().getProjection().getExtent()
map.getView().fitExtent(extent, map.getSize());
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Comment OpenLayers sait-il qu'il faut interroger l'habituelle couche public.world_simple ?

Avec Firefox RESTClient (ou autre équivalent), lancer la requête POST suivante sur le serveur WMS :

<?xml version="1.0" encoding="UTF-8"?>
<DescribeFeatureType version="1.1.0" service="WFS"
xmlns="http://www.opengis.net/wfs"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd">
<TypeName>world_simple</TypeName>
</DescribeFeatureType>

puis lancer

<?xml version="1.0" encoding="UTF-8"?>
<GetFeature version="1.1.0" service="WFS"
xmlns="http://www.opengis.net/wfs"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd">
<Query typeName="world_simple">
<PropertyName>region</PropertyName>
</Query>
</GetFeature>

Créer votre propre pinkWorld.sld.xml en y insérant l'instruction Filter suivante en début de Rule (ex. mettre ce fichier dans un espace type Dropbox pour que le serveur WMS puisse y accéder)

...
<Rule>
<Filter xmlns="http://www.opengis.net/ogc">
<PropertyIsEqualTo>
<PropertyName>region</PropertyName>
<Literal>NorthAfrica</Literal>
</PropertyIsEqualTo>
</Filter>
<PolygonSymbolizer>
...

Ajouter la Rule suivante après la première :

...
</Rule>
<Rule>
<ElseFilter/>
<PolygonSymbolizer>
<Fill>
<CssParameter name="fill">#DDDDDD</CssParameter>
<CssParameter name="fill-opacity">1.0</CssParameter>
</Fill>
<Stroke>
<CssParameter name="stroke">#000000</CssParameter>
<CssParameter name="stroke-width">1</CssParameter>
</Stroke>
</PolygonSymbolizer>
</Rule>
</FeatureTypeStyle>
...

Modifier le style pour que la Suisse soit représentée comme ci-dessous.

voir la spécification Styled Layer Descriptor

et le tutoriel SLD

Ex3C : Custom tiling service with CartoCSS

Introduction avec MapBox et exercices : cartocss.pdf

Symbol drawing order : schema & documentation

<html>
<head>
<title>ol3 - Ex3C - webmap with your own MapBox basemap</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function() {
var token = "pk.eyJ1IjoiamZyZXlyZSIsImEiOiIwMnZKUHR3In0.8YWGPOTZ3bFglVmnCvWjvw";
var styleId = "jfreyre.c74df3e5";
map = new ol.Map({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'http://api.tiles.mapbox.com/v4/'+styleId+'/{z}/{x}/{y}.png?access_token=' + token
})
})
],
target: 'map',
view: new ol.View({
center: ol.proj.transform([6.7, 46.7], 'EPSG:4326', 'EPSG:3857'),
zoom: 10
})
});
})
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Modifier le code nécessaire ci-dessus pour créer une application geoweb utilisant votre style MapBox.

4. Composition de couches

Un intérêt d'une carte en ligne est de pouvoir la composer à partir de plusieurs couches.

Ex4A : Composition "server-side"

La composition peut se faire par un assemblage côté serveur. L'Ex3C a déjà bien montré comment préparer un tel assemblage. Ci-dessous un assemblage “à la demande”. Notez bien, plus il y a de couches disponibles sur le serveur cartographique, plus il y a de combinaisons.... Problèmes... Impossible de retourner la map demandée

<html>
<head>
<title>ol3 - Ex4A - 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: "https://eu1.mapcentia.com/wms/oertz/public",
params:{
VERSION: "1.1.1",
LAYERS: 'public.world_simple,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

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

Trouver un tel type d'assemblage parmi les Basic Map Types de l'API Google Maps.

Ex4B : Composition "client-side"

Un cadre applicatif de cartographie en ligne se doit de fournir la capacité de gérer la composition côté client. C'est le cas d'OpenLayers comme ci-dessous :

<html>
<head>
<title>ol3 - Ex4A - one WMS request for two layers</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;
$(document).ready(function(){
map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Image({
'title': 'Base maps',
source: new ol.source.ImageWMS({
url: "https://eu1.mapcentia.com/wms/oertz/public",
title: 'World simple',
type:'base',
params:{
VERSION: "1.1.1",
LAYERS: 'public.world_simple',
FORMAT: 'image/png'
}
})
}),
new ol.layer.Image({
title: 'Cities',
source: new ol.source.ImageWMS({
url: "https://eu1.mapcentia.com/wms/oertz/public",
params:{
VERSION: "1.1.1",
LAYERS: 'public.cities',
FORMAT: 'image/png'
}
})
})
]
});
var layerSwitcher = new ol.control.LayerSwitcher();
map.addControl(layerSwitcher);
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

Est-ce vraiment ce que l'on veut (remarquer le nouveau contrôle apparu dans l'IHM) ?

Comment faire pour vraiment assembler/superposer ces deux couches côté client ?

Partant de Ex2B, créer une application de webmapping avec la composition suivante :

une couche de base Bing Maps (ex. Aerial)

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

Ajouter en superposition votre style MapBox représentant les communes de Suisse (cf. Ex3C - Introduction) comme ci-dessous :
Illustration du résultat attendu :

Remplacer la couche de base Bing Maps par la couche MapBox examples.map-i86nkdio

Ex4C : On-the-fly CartoCSS with CartoDB

Jusqu'à présent, les styles CartoCSS devaient être préparés et chargés avec le Studio vers un compte MapBox. La suite montre une approche plus souple permettant de définir “programmatiquement” le style à appliquer sur une couche de données chargées sur un compte CartoDB.

<html>
<head>
<title>ol3 - Ex4C - webmap with tiled layer</title>
<script src="http://libs.cartocdn.com/cartodb.js/v3/3.11/cartodb.core.js"></script>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
map = new ol.Map({
view: new ol.View({
center:ol.proj.transform([7,47], 'EPSG:4326', 'EPSG:3857'),
zoom: 13
}),
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
]
})
cartodb.Tiles.getTiles({
type: 'cartodb', user_name: 'ogo', sublayers: [{
sql: 'SELECT * FROM g4districts98',
cartocss: "#g4districts98 { line-color: red; line-width: 2;}"
}]
}, function(tileTemplate) { // create layer with generated tiles URL
tilesUrl = tileTemplate.tiles[0]
.replace('{s}', 'a')
.replace('{z}', '{z}')
.replace('{x}', '{x}')
.replace('{y}', '{y}');
admLyr = new ol.layer.Tile({
source: new ol.source.XYZ({
url: tilesUrl
})
})
map.addLayer(admLyr);
});
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Adapter le style pour afficher les districts Vaudois différemment des autres (utiliser l'attribut ak avec sa valeur 'VD')

Adapter l'application pour que le style CartoCSS soit chargé synchronnement depuis un fichier monStyle.mss.

5. Couche "vector"

Dans les sections précédentes nous avons inséré des couches cartographique sous la forme d'images construites par le serveur avec son moteur cartographique. Un serveur cartographique était donc nécessaire.

A l'opposé, pour une couche OpenLayers.Layer.Vector c'est le client qui joue le rôle de moteur cartographique pour créer la visualisation d'entités géographiques (Features en anglais).

Ex5A : Source de données OpenLayers.Format

Une couche ol.layer.Vector peut être alimentée par une source de données au format ol.source.GeoJSON (voir aussi http://geojson.org). Il existe de nombreux autres formats : ol.source.GPX, ol.source.TopoJSON, ol.source.KML, ...)

<html>
<head>
<title>ol3 - Ex5A - GeoJSON vector overlay</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
map = new ol.Map({
view: new ol.View({
center:[738600, 5840171],
zoom: 4
}),
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
]
})
var vectorLayer = new ol.layer.Vector({
source: new ol.source.GeoJSON({
projection: 'EPSG:3857',
url: "data/4capitals.json"
})
})
map.addLayer(vectorLayer)
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Apporter la preuve que les données sont bien transformées en objets graphiques côté client !

Remplacer l'URL source par

http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson

Dans une div HTML, afficher la liste des descriptions des séismes --> corrigé : url?

Modifier l'url comme suit et modifier le type de source de données en conséquence:

url: 'http://www.visugpx.com/files/download.php?id=1416844831'

en recentrant la carte par ici :

view: new ol.View({
center:[766225, 5908730],
zoom: 10
}),

Pourquoi cela ne fonctionne pas?

Notons aussi le Geographic Markup Language de l'OGC qui “consiste en un ensemble de schémas XML qui définissent un format ouvert pour l'échange de données géographiques et permettent de construire des modèles de données spécifiques pour des domaines spécialisés, comme l'urbanisme, l'hydrologie ou la géologie” (source Wikipedia)
Exemple : http://geoc.heig-vd.ch/ogo/olmap/data/fourCapitals.gml

Ex5B : ol.source.Vector sous la loupe

L'exemple ci-dessous décrit en détail ce qu'est une source de donnée ol.source.Vector pour une couche ol.layer.Vector en montrant comment la créer et l'alimenter à partir d'un format de source de données “fait maison”.

En résumé, une source ol.source.Vector est composée d'entités géographiques (ol.Feature) chacune composée d'une géométrie (ol.geom) et d'une liste d'attributs.

La source de données est un fichier texte formaté comme suit:

2.3332999999999999 48.866700000000002 Paris,7.4333 46.950000000000003 Bern,12.5 41.883299999999998 Rome,-3.71 40.409999999999997 Madrid

La structure de formatage est simple : c'est une suite de triplets “Longitude Latitude Nom” dont le séparateur interne est l'espace. Le séparateur entre triplets est la virgule. Chaque triplet correspond à une entité géographique.

Le code ci-dessous déchiffre cette structure et transforme les triplets en OpenLayers.Feature.Vector.

<html>
<head>
<title>ol3 - Ex5B - custom format for vector overlay</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
map = new ol.Map({
view: new ol.View({
center:ol.proj.transform([2,48], 'EPSG:4326', 'EPSG:3857'),
zoom: 5
}),
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
}),
]
})
$.ajax({
type: "GET",
url: "../data/4capitals.txt",
dataType: "text",
success: createCustomOverlay
});
function createCustomOverlay(response){
// Créer/ajouter une nouvelle couche vide
vectors = new ol.layer.Vector({
source: new ol.source.Vector()
})
map.addLayer(vectors)
// Préparer un tableau pour accueillir des entités géographiques (des "features")
features = new Array();
// On décompose notre flux géographique encodé "maison"
tabCapitals = response.split(",");
for(i = 0; i < tabCapitals.length; i++){
tabCapital = tabCapitals[i].split(" ");
// on construit et ajoute au tableau une feature composé d'une géométrie (ici Point)
var feature = new ol.Feature({
// que le constructeur utilise pour alimenter la propriété feature.geometry
geometry: new ol.geom.Point(ol.proj.transform([parseFloat(tabCapital[0]),parseFloat(tabCapital[1])],'EPSG:4326', 'EPSG:3857')),
name: tabCapital[2]
});
features.push(feature)
}
vectors.getSource().addFeatures(features)
}
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Inspecter l'objet OpenLayers qui gère la persistence de ces entités géographiques côté client (ex. avec Firebug)

Adapter le code pour une source de données similaire contenant un attribut de plus comme suit :

2.3332999999999999 48.866700000000002 Paris France,7.4333 46.950000000000003 Bern Suisse,12.5 41.883299999999998 Rome Italy,-3.71 40.409999999999997 Madrid Spain

et réinspecter pour vérifier.

6. Style "vector"

OpenLayers met à disposition une classe ol.style permettant d'appliquer des styles aux features (entités géographiques) d'une couche ol.vector.

Ex6A : Style d'entité

Openlayers applique un style par défaut aux features, mais il est possible de redéfinir ce style (forme, couleur, etc.).

<html>
<head>
<title>ol3 - Ex6A - One feature, one style</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
map = new ol.Map({
view: new ol.View({
center:ol.proj.transform([2,48], 'EPSG:4326', 'EPSG:3857'),
zoom: 5
}),
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
]
})
vectLayer = new ol.layer.Vector({
source: new ol.source.Vector()
})
map.addLayer(vectLayer)
var stroke = new ol.style.Stroke({
color: '#ff0000',
width: 3
});
var style = new ol.style.Style({
image: new ol.style.RegularShape({
points: 4,
stroke: stroke,
radius: 10,
radius1:0,
radius2:0
}),
stroke: stroke
})
var feature = new ol.Feature({
geometry: new ol.geom.Point([738600, 5840171])
});
feature.setStyle(style)
vectLayer.getSource().addFeature(feature)
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Remplacer le style avec les propriétés suivantes :

var style = new ol.style.Style({
image: new ol.style.Icon({
src:"http://www.cretasolaris.gr/gfx/marker.png",
size:[36,33],
opacity:1
})
})

Ex6B : Style de couche

L'approche la plus répandue consiste à associer un style à une couche.

<html>
<head>
<title>ol3 - Ex6B - One layer, one style</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
var fill = new ol.style.Fill({
color: '#acc000'
});
var stroke = new ol.style.Stroke({
color: '#ff0000',
width: 3
});
var style = new ol.style.Style({
image: new ol.style.Icon({
src:"http://www.cretasolaris.gr/gfx/marker.png",
size:[36,33],
opacity:1
})
})
map = new ol.Map({
view: new ol.View({
center:ol.proj.transform([2,48], 'EPSG:4326', 'EPSG:3857'),
zoom: 5
}),
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
]
})
vecLayer = new ol.layer.Vector({
source: new ol.source.Vector({}),
style: style
})
var feature = new ol.Feature({
geometry: new ol.geom.Point([738600, 5840171]),
name: "The precious is here!",
author: "Gollum"
});
map.addLayer(vecLayer)
vecLayer.getSource().addFeature(feature)
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Ajouter une entité géographique de plus comme suit :

var feature2 = new ol.Feature({
geometry: new ol.geom.Point([1938600, 6840171]),
name: "The hobbits are far away...",
author: "Gandalf"
});
vecLayer.getSource().addFeature(feature2)

Ex6C : Fonction de style

Plutôt que d'appliquer un style identique à toutes les entités d'une couche, il est possible d'appliquer un style différents basé sur les propriétés de chacune des entités. Pour cela, Openlayers propose de remplacer l'objet ol.style.Style par une fonction ol.style.StyleFunction(). Dans l'exemple ci-dessous on affiche un titre différent en fonction de la propriété “name” de chaque entité.

<html>
<head>
<title>ol3 - Ex6C - Styling function</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
var fill = new ol.style.Fill({
color: '#acc000'
});
var stroke = new ol.style.Stroke({
color: '#ff0000',
width: 3
});
var icon = new ol.style.Icon({
src:"http://www.cretasolaris.gr/gfx/marker.png",
size:[36,33],
opacity:1
})
function createLordStyle(){
return function(feature, resolution){
var style = new ol.style.Style({
image: icon,
text: new ol.style.Text({
text:feature.get('name'),
offsetY: 25,
font: '15px Calibri,sans-serif',
fill: new ol.style.Fill({
color: "#000"
})
})
})
return [style]
}
}
map = new ol.Map({
view: new ol.View({
center:ol.proj.transform([2,48], 'EPSG:4326', 'EPSG:3857'),
zoom: 5
}),
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
]
})
vecLayer = new ol.layer.Vector({
source: new ol.source.Vector(),
style: createLordStyle()
})
var feature = new ol.Feature({
geometry: new ol.geom.Point([738600, 5840171]),
name: "The precious is here!",
author: "Gollum"
});
map.addLayer(vecLayer)
var feature2 = new ol.Feature({
geometry: new ol.geom.Point([1938600, 6840171]),
name: "The hobbits are far away...",
author: "Gandalf"
});
vecLayer.getSource().addFeatures([feature,feature2])
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

TODO

Ajouter une couche vectorielle de données au format WFS.

var wfsFormat = new ol.format.WFS({
featureNS:"http://mediamaps.ch/geoinf",
featureType:"cities"
})
var vectorSource = new ol.source.ServerVector({
format: new ol.format.WFS({
featureNS:"http://twitter/oertz",
featureType:"cities"
}),
loader: function(extent, resolution, projection) {
var url = 'http://geoserver-heig.rhcloud.com/geoinf/ows?service=WFS&version=1.1.0&request=GetFeature&typeName=geoinf:cities&srsname=EPSG:3857'
$.ajax({
url: url
})
.done(function(response) {
vectorSource.addFeatures(wfsFormat.readFeatures(response));
});
},
strategy: ol.loadingstrategy.all,
projection: 'EPSG:4326'
});
var citiesLayer = new ol.layer.Vector({
source: vectorSource
})

Openlayers applique un style par défaut aux features ol.style.

TODO

Appliquer un style différent en fonction de la région géographique (geo_region) des entités.

Reprendre Ex5A avec le flux http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson et un style basé sur une fonction de contexte pour représenter par classification l'intensité des séismes (exploiter l'attribut mag, voir aussi Echelle de Richter).

7. Feature access and controls

OpenLayers offre un ensemble de fonctionnalités permettant d'interagir avec la carte. Par défaut, les interactions de zoom et pan sont actives mais il en existe d'autres (ol.control) En plus de la gestion événementielle offerte par ces contrôleurs, OpenLayers permet d'enregistrer des écouteurs sur de nombreux événements, comme ceux en lien avec la vie d'un ol.Map (voir ol.MapBrowserEvents). Maintenant que les techniques de visualisation cartographique sont maîtrisées, tant côté serveur que client, il s'agit de découvrir comment interagir avec les objets géographiques représentés.

Ex7A : contrôle d'interrogation en mode image (WMS GetFeatureInfo)

Cet exemple illustre l'interrogation des objets de la carte par le simple clic d'un pixel sur la carte. On enregistre un écouteur sur la map à l'aide de la méthode ol.Map.on afin d'exécuter du code lors d'un click sur la map. On utilise ensuite la méthode ol.source.ImageWMS.getGetFeatureInfoUrl pour construire l'URL qui permet d'interroger le WMS. Enfin, la fonction getFeatureInfo(url) permet d'envoyer une requête au serveur WMS à l'aide de l'URL précédemment construite.

<html>
<head>
<title>ol3 - Ex7A - interaction with WMS GetFeatureInfo control</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
var wmsSource = new ol.source.ImageWMS({
url: 'https://eu1.mapcentia.com/wms/oertz/public',
params:{
VERSION: "1.1.1",
LAYERS: 'public.world_simple',
FORMAT: 'image/png'
},
serverType: 'mapserver',
});
var wmsLayer = new ol.layer.Image({
source: wmsSource
});
var view = new ol.View({
center: [0, 0],
zoom: 1
});
map = new ol.Map({
layers: [wmsLayer],
target: 'map',
view: view
});
map.on('singleclick', function(evt) {
var viewResolution = (view.getResolution());
var url = wmsSource.getGetFeatureInfoUrl(
evt.coordinate, viewResolution, 'EPSG:3857',
{'INFO_FORMAT': 'text/plain'});
if (url) {
getFeatureInfo(url);
}
});
function getFeatureInfo(url){
var url2 = encodeURIComponent(url);
var request = $.ajax({
url: "http://localhost:8888/openlayers/ol2014-workshop/proxy.php?url="+url2,
dataType: 'text',
contentType: 'text/plain',
method: "GET"
});
request.done(function( data ) {
console.log(data)
$("#info").html(data.replace(":", "<br>"));
});
request.fail(function( jqXHR, textStatus ) {
alert( "Request failed: " + textStatus );
});
}
map.zoomToMaxExtent();
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
#info {
position: absolute;
top: 20px;
left: 50px;
background-color: white;
border: solid gray 1px;
padding: 5px;
font-size: smaller;
z-index: 1000000000;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="info">Click on the map to get feature info</div>
</body>
</html>

TODO

Analyser l'échange client/serveur (ex. avec Firebug). Comment est-ce possible ?

Ajouter des paramètres dits “vendor” comme ci-dessous. Quel est l'effet ?

var url = wmsSource.getGetFeatureInfoUrl(
evt.coordinate, viewResolution, 'EPSG:3857',
{'INFO_FORMAT': 'text/plain','radius':50}
);

Ex7B : un contrôle d'interrogation image "fait maison"

Dans Ex7A, c'est une version standardisé du contrôle d'interrogation lié au standard OGC WMS. L'exemple suivant montre comment mettre cela en oeuvre par nous-même. On s'assure ainsi de maîtriser tout le processus client/serveur.

Côté client:

<html>
<head>
<title>Ex7B - homemade GetFeatureInfo</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
var wmsSource = new ol.source.ImageWMS({
url: 'https://eu1.mapcentia.com/wms/oertz/public',
params:{
VERSION: "1.1.1",
LAYERS: 'public.world_simple',
FORMAT: 'image/png'
},
serverType: 'mapserver',
});
var wmsLayer = new ol.layer.Image({
source: wmsSource
});
var view = new ol.View({
center: [0, 0],
zoom: 1,
projection: "EPSG:4326"
});
map = new ol.Map({
layers: [wmsLayer],
target: 'map',
view: view
});
map.on('singleclick', function(evt) {
getFeatureInfo(evt.coordinate);
});
function getFeatureInfo(coordinates){
var request = $.ajax({
url: getInfo = "php/GetCountriesByXY.php?x=" + coordinates[0] + "&y=" + coordinates[1],
dataType: 'json',
method: "GET"
});
request.done(function( data ) {
$("#info").html(data.features[0].properties.name);
});
request.fail(function( jqXHR, textStatus ) {
alert( "Request failed: " + textStatus );
});
}
});
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
#info {
position: absolute;
top: 20px;
left: 50px;
background-color: white;
border: solid gray 1px;
padding: 5px;
font-size: smaller;
z-index: 1000000000;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="info">Click on the map to get feature info</div>
</body>
</html>

Côté serveur:

<?php
require_once 'GeoManager.php';
$XY = $_GET["x"] . " " . $_GET["y"];
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$api_key = "2e957be5f1e9c0793ca1725aab08274a795f2c3f";
$q = "SELECT name, ST_AsGeoJSON(the_geom) AS geom FROM world_simple"
. " WHERE ST_Intersects(the_geom, ST_GeomFromText('POINT(" . $XY . ")', 4326))";
$url = "http://oertz.cartodb.com/api/v2/sql?api_key=" . $api_key. "&q=" . urlencode($q);
curl_setopt($ch, CURLOPT_URL, $url);
$result = curl_exec($ch);
$rows = json_decode($result)->rows;
header("Content-type: application/json");
$i = 0;
$fc = new FeatureCollection();
foreach ($rows as $row) {
$fc->addFeature(new Feature($i++, json_decode($row->geom), array("name" => $row->name)));
}
echo json_encode($fc);
?>

TODO

Côté client, ajouter la ligne ci-dessous au bon endroit :

$("#info").html("Loading... please wait...");

Ajouter en fin de fonction ready une couche OpenLayers.Layer.Vector comme suit :

var vectorLayer = new ol.layer.Vector({source: new ol.source.Vector({})})
map.addLayer(vectorLayer)

et remplacer l'appel AJAX comme suit :

var request = $.ajax({
url: getInfo = "php/GetCountriesByXY.php?x=" + coordinates[0] + "&y=" + coordinates[1],
dataType: 'json',
method: "GET"
});
request.done(function( data ) {
var js = new ol.format.GeoJSON()
$("#info").html(data.features[0].properties.name);
var feature = js.readFeature(data.features[0])
vectorLayer.getSource().addFeature(feature)
});
request.fail(function( jqXHR, textStatus ) {
alert( "Request failed: " + textStatus );
});

Ajouter en fin de fonction ready l'interaction suivante avec son écouteur :

var selectPointerMove = new ol.interaction.Select({
condition: ol.events.condition.pointerMove
});
map.addInteraction(selectPointerMove)
selectPointerMove.on('select',function(e){
if(e.target.getFeatures().getLength() > 0){
$("#info").html(e.target.getFeatures().item(0).get('name'));
}
})
map.addInteraction(selectPointerMove)

Les interactions permettent de récupérer et d'exploiter des actions déclenchées par l'utilisateur (via son clavier ou sa souris). Ces actions peuvent ensuite être utilisées pour modifier les différents éléments sur la map ou la map elle même. Dans notre cas, notre interaction permet de sélectionner (ol.interaction.Select) les features de notre carte au passage de la souris (condition: ol.events.condition.pointerMove) sur celles-ci.

Enregistrer un écouteur sur le bon événement pour vider la <div> info quand le curseur sort d'un feature.

Ex7C : stratégies de chargement en mode vector

Le chargement d'entités géographiques peut s'avérer néfaste en terme d'expérience utilisateur du fait d'une latence provoquée (1) par le volume important de données à charger pour une bande passante donnée et (2) par les performances et l'aptitude du moteur de rendu cartographique à digérer ces données.

L'exemple suivant montre déjà des limites avec la stratégie ol.loadingstrategy.all qui charge simplement tous les objets dans l'enveloppe de visualisation. Il s'agit alors d'opter pour des stratégies afin d'éviter ces écueils.

<html>
<head>
<title>Ex7C - Vector overlay loading strategies</title>
<script type="text/javascript" src="js/config.js"></script>
<script type="text/javascript">
var map;
$(document).ready(function(){
var osmLayer = new ol.layer.Tile({
source: new ol.source.OSM()
});
var view = new ol.View({
center: ol.proj.transform([8,47],"EPSG:4326","EPSG:3857"),
zoom: 5
});
map = new ol.Map({
layers: [osmLayer],
target: 'map',
view: view
});
var wfsFormat = new ol.format.WFS({
featureNS:"http://mediamaps.ch/geoinf",
featureType:"cities"
})
var vectorSource = new ol.source.ServerVector({
format: wfsFormat,
loader: function(extent, resolution, projection) {
var url = 'http://geoserver-heig.rhcloud.com/geoinf/ows'
$.ajax({
url: url,
data:{
service: 'WFS',
version: '1.1.0',
request: 'GetFeature',
typename: 'cities',
srsname: 'EPSG:3857'
},
dataType: 'text',
success: function(response) {
vectorSource.addFeatures(wfsFormat.readFeatures(response));
},
error: function (jqXHR, textStatus, errorThrown) {
alert("The request has failed: " + textStatus + errorThrown);
}
});
},
strategy: ol.loadingstrategy.all,
projection: "EPSG:3857"
});
var lyr = new ol.layer.Vector({
source: vectorSource,
})
map.addLayer(lyr)
})
</script>
<style type="text/css">
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="info"></div>
</body>
</html>

TODO

Compter les appels au serveur géographique et vérifier le(s) volume(s) des entités géographiques transférées.

Faire de même avec le chargement de la couche european_nuts en modifiant les paramètres ci-dessous :

featureType:"european_nuts"

pour le format et

typename: 'european_nuts'

dans la requête ajax.

Modifier la stratégie comme suit :

ol.loadingstrategy.bbox

ajouter le paramètre suivant à la requête ajax

bbox: extent.join(',') + ',EPSG:3857'

et tout en naviguant dans la carte, compter les appels au serveur géographique et revérifier le(s) volume(s) des entités géographiques transférées.

Rendre la couche vector invisible au chargement de l'application en insérant l'instruction comme indiqué ci-dessous :

var lyr = new ol.layer.Vector({
source: vectorSource,
visible: false
})

de plus, ajouter les instructions ci-dessous en fin de fonction ready :

view.on('change:resolution',function(){
if(view.getZoom() > 8){
lyr.setVisible(true)
}else{
lyr.setVisible(false)
}
})

Quelles autres stratégies peut-on imaginer pour assurer une bonne expérience utilisateur ?

Ex7D : contrôles en mode vector

Le chargement d'entités géographiques (plutôt que leur représentation graphique image) offre des possibilités d'interaction plus directe sur les objets vectoriels qui les représentent. Voyons quelques exemples.

TODO

Sur la base de Ex7C avec la couche cities, définir les styles defaultPoint et selectPoint suivant :<code>var defaultPoint = new ol.style.Style({

image: new ol.style.Circle({

fill:fill,
radius: 6
})
})

var selectPoint = new ol.style.Style({
image: new ol.style.Circle({
fill:hoverFill,
radius: 6
})
})