GIS and Media fusion

"The explosive growth of the GeoWeb and geographic information has made GIS powerful media for the general public to communicate, but perhaps more importantly, GIS have also become media for constructive dialogs and interactions about social issues." - Sui & Goodchild

User Tools

Site Tools


geoinf18:oltuto4

OpenLayers3 (2018, part 4)

8. Contrôles et interactions

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. 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 la carte, ses couches et les objets géographiques représentés.

A. Gestion événementielle cartographique

Cet exemple démontre qu'il est nécessaire qu'un gestionnaire s'occupe spécifiquement d'évenements cartographiques comme ceux en lien avec la vie d'un ol.Map ou ceux relatifs à la vie d'une ol.layer.Vector, d'une ol.source.Vector, etc.

<html>
    <head>
        <title>ol3 - 8a - ol.ObjectEvent</title>
        <script type="text/javascript" src="js/config.js"></script>
        <script type="text/javascript">
            var map, vectorLayer;

            $(document).ready(function () {
                map = new ol.Map({
                    view: new ol.View({
                        center: [0, 0],
                        zoom: 2
                    }),
                    target: 'map',
                    layers: [
                        new ol.layer.Tile({
                            source: new ol.source.OSM()
                        })
                    ]
                });

                map.on('pointermove', function (e) {
                    $("#info").html(
                      "Pixel:"
                      + e.pixel +
                      " => Lon/Lat: "
                      + ol.proj.transform(e.coordinate,"EPSG:3857","EPSG:4326")
                    );
                });
            });
        </script>

        <style type="text/css">
            #map {
                width: 100%;
                height: 100%;
            }

            #info {
                position: absolute;
                top: 15px;
                left: 60px;
                background-color: #fff;
                padding: 5px;
                font-size: small;
            }

            p {
                margin: 0;
            }
        </style>
    </head>
    <body>
        <div id="map"></div>
        <div id="info"></div>
    </body>
</html>

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

  1. Utiliser l'événement singleclick pour s'aider à définir un centrage de carte sur un point de votre choix (par exemple les Pierres du Niton)
  2. De nombreuses propriétés des classes OpenLayers sont “observables”, comme celles de la classe ol.View. Ajouter ce qui suit :
    // Set listener on change:center (ol.ObjectEvent)
    map.getView().on('change:center', function (e) {
        $("#info").html("Center of the map: " + e.target.getCenter());
    });
  3. Adapter l'application pour que soit automatiquement utilisé comme baselayer une couche Bingmaps dès que le niveau de zoom passe à une échelle plus grande que le niveau 6 (à la http://urbangene.heig-vd.ch).
  4. Ajouter une couche vector (que l'on nomme quakeLayer) avec les points séismes du point 6A :
    1. Comme au point 6A on souhaite afficher la liste des descriptions des séismes, mais cette fois-ci sans rien imposer à l'utilisateur (keypress), autrement dit que la liste s'affiche automatiquement. Comment faire ? Qu'est-ce qui ne marche justement pas ?
    2. Quel listener de la classe ol.source.Vector peut aider ?
      // May be we set a listener, but which one ?
      quakeLayer.getSource().on("???", function (e) {
          // ???
      });

B. Interrogation en mode image (GetFeatureInfo)

Le gestionnaire d'événements cartographiques va notamment servir à déployer la dernière fonctionnalité de base d'une application de webmapping, celle qui consiste à interroger les objets de la carte. Et d'abord en mode image, c'est-à-dire comment avoir des informations (attributaires) sur l'entité géographique que représente un pixel (“cachée derrière” ce pixel) ?

L'application ci-dessous 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 l'habituel ol.Map.on et afin d'exécuter une action au click sur la map. Cette action utilise alors la méthode ol.source.ImageWMS.getGetFeatureInfoUrl qui est un générateur de requête WMS GetFeatureInfo, il construit pour nous l'URL qui permet d'interroger le WMS selon contexte de clic et de visualisation. Enfin, la fonction getFeatureInfo(url) permet de gérer l'aller/retour avec le serveur WMS avec cette URL précédemment construite.

<html>
    <head>
        <title>ol3 - 8b - interaction with WMS GetFeatureInfo operation</title>
        <script type="text/javascript" src="js/config.js"></script>
        <script type="text/javascript">
            var map, wmsLayer, vectors;

            $(document).ready(function () {
                map = new ol.Map({
                    target: 'map',
                    view: new ol.View({
                        center: ol.proj.transform([6.7, 46.7], 'EPSG:4326', 'EPSG:3857'),
                        zoom: 4
                    })
                });

                var wmsLayer = new ol.layer.Image({
                    source: new ol.source.ImageWMS({
                        url: "https://demo.boundlessgeo.com/geoserver/opengeo/wms",
                        ratio: 1,
                        params: {
                            VERSION: "1.0.0",
                            LAYERS: "opengeo:countries",
                            FORMAT: "image/png"
                        }
                    })
                });
                map.addLayer(wmsLayer);

                map.on('singleclick', function (evt) {
                    var wmsSource = wmsLayer.getSource();

                    // WMS GetFeatureInfo request generator
                    var url = wmsSource.getGetFeatureInfoUrl(
                            // clic context
                            evt.coordinate,
                            // map context
                            map.getView().getResolution(),
                            "EPSG:3857",
                            {
                                INFO_FORMAT: "text/html"
                            }
                    );
                    if (url) getFeatureInfo(url);
                });
            });

            function getFeatureInfo(url) {
                var request = $.ajax({
                    url: url,
                    dataType: "text"
                });

                request.done(function (data) {
                    $("#info").html(data);
                });

                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: 60px;
                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 https://www.mediamaps.ch/oltuto/Ex8b.html

  1. Un serveur WMS peut formatter la réponse de différentes manières (voir la configuration du serveur). Par exemple, modifier INFO_FORMAT comme suit :
    INFO_FORMAT: "application/json"

    Que se passe-t-il à l'interrogation de la carte ? Regardez le flux client/serveur … et exploitez-le pour n'afficher que le nom de pays.

  2. Avec ce format GeoJSON on peut imaginer une interrogation encore plus intéressante, en deux étapes et mettant en évidence visuellement l'entité interrogée.
    1. Ajouter une couche vector :
      vectorLayer = new ol.layer.Vector({
          source: new ol.source.Vector()
      });
      map.addLayer(vectorLayer);
    2. Modifier le cas request.done de la fonction getFeatureInfo comme suit :
      request.done(function (data) {
          decoder = new ol.format.GeoJSON();
          features = decoder.readFeatures(data);
      
          if (features.length > 0) {
              vectorLayer.getSource().clear();
              vectorLayer.getSource().addFeatures(features);
              $("#info").html(features[0].get("admin") + " (" + features[0].get("income_grp") + ")");
          }
      });

C. Interaction avec ol.layer.Vector

Après les utilisations possibles pour interagir avec les entités représentées par les services cartographiques, voyons comment interagir directement avec les entités vectors “vivantes” client-side. On utulise pour cela le concept d'ol.interaction, notamment ol.interaction.Select.

<html>
    <head>
        <title>ol3 - 8c - Interaction sur entités vectors</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: 3
                    }),
                    target: 'map',
                    layers: [
                        new ol.layer.Tile({
                            source: new ol.source.OSM()
                        })
                    ]
                });
                vectorLayer = new ol.layer.Vector({
                    source: new ol.source.Vector({
                        url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson",
                        format: new ol.format.GeoJSON()
                    })
                });
                map.addLayer(vectorLayer);

                var selectInteraction = new ol.interaction.Select({
                    condition: ol.events.condition.singleClick,
                    // the interactive layers on which the selection is possible (they may be more than one)
                    layers: [vectorLayer]   
                });
                map.addInteraction(selectInteraction);

                // add a listener to fire when one or more feature from the interactive layer(s) is(are) selected
                selectInteraction.on('select', function (e) {
                    if(e.selected.length > 0) {
                        var title = e.selected[0].get("title");
                        $("#info").html(title);
                    }
                });
            });
        </script>

        <style type="text/css">
            #map {
                width: 100%;
                height: 100%;
            }
            #info {
                position: absolute;
                top: 20px;
                left: 60px;
                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">Select on the map to get feature info</div>
    </body>
</html>

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

  1. Quel événement l'ol.events.condition utiliser pour avoir le comportement type “onmouseover” ? Sans oublier de mettre l'IHM en cohérence (voir ol.interaction.Select.Event select).
  2. Configurer l'interaction de sorte que l'utilisateur puisse aussi cliquer sur la bulle d'information pour aller à la page des détails du séisme (cf propriété url, ex. https://earthquake.usgs.gov/earthquakes/eventpage/nc72722700)

D. Géoservice d'interrogation personnalisé

En mode vector, l'interrogation n'a plus besoin du serveur pour révéler des données attributaires des entités représentées sur la carte. Par contre, comme déjà vu au point B, en mode image, pour que le “pixel révèle” l'information sous-jacente, le serveur doit être sollicité. Le point B fait usage du géoservice standardisé WMS et son opération GetFeatureInfo. Ci-dessous est proposé un géoservice personnalisé qui montre tout le processus d'interrogation, remontant jusque dans la base de données. CARTO (aka cartodb) est utilisé pour ses deux géoservices suivants :

  • d'abord comme serveur cartographique pour livrer une représentation des cabanes selon des instructions cartographiques pilotées client-side
  • puis comme serveur d'entités géographiques pour livrer les données attributaires d'une cabane selon un clic sur la carte (et donc des coordonnées)

Cette application montre tout le potentiel d'une application de webmapping associé à un géoservice capable de “calculer une géoinformation” (ex. calculer d'un itinéraire, geocoding, geofencing, …).

<html>
    <head>
        <title>ol3 - Ex6a - GeoJSON vector overlay</title>
        <script type="text/javascript" src="js/config.js"></script>
        <script src="http://libs.cartocdn.com/cartodb.js/v3/3.15/cartodb.core.js"></script>
        <script type="text/javascript">
            var map;
            $(document).ready(function(){
                map = new ol.Map({
                    view: new ol.View({
                        center:[738600, 5840171],
                        zoom: 10
                    }),
                    target: 'map',
                    layers: [
                      new ol.layer.Tile({
                          source: new ol.source.BingMaps({
                              key: 'AqE05oJsq-bWa50FPOW2S0eQm9Oqqygc1VTi_WPhUIoKR_-jgA559CRbfndgWAIz',
                              imagerySet: 'Aerial'
                          })
                      })
                    ]
                });

                // Using cartodb geoservice for server-side cartography
                cartodb.Tiles.getTiles({
                    type: 'cartodb', user_name: 'ogo', sublayers: [{
                            sql: 'SELECT * FROM cabanes4326_merge',
                            cartocss: "#layer {  marker-width: 20;  marker-file: url(https://www.camptocamp.org/static/4ef52c7/img/documents/waypoints/hut.svg);  }"
                        }]
                }, function (tileTemplate) { // create layer with generated tiles URL
                    tilesUrl = tileTemplate.tiles[0]
                            .replace('{s}', 'a')
                            .replace('{z}', '{z}')
                            .replace('{x}', '{x}')
                            .replace('{y}', '{y}');

                    console.log(tilesUrl);

                    hutLayer = new ol.layer.Tile({
                        source: new ol.source.XYZ({
                            url: tilesUrl
                        })
                    })
                    map.addLayer(hutLayer);
                });

                map.on('singleclick', function (e) {
                    getFeatureInfo(ol.proj.transform(e.coordinate,"EPSG:3857","EPSG:4326"));
                });
            });

            function getFeatureInfo(lonlat) {
                    // Using cartodb geoservice for querying with SQL API
                    // E.g. SELECT * FROM cabanes4326_merge WHERE ST_Distance(the_geom, ST_GeomFromText('POINT(6.42160 46.0101)', 4326)) < 0.01

                    var radius = 0.01; // 1° ~111km (equator)
                    var sqlquery = "SELECT title, the_geom FROM cabanes4326_merge WHERE ST_Distance(the_geom, ST_GeomFromText('POINT(" + lonlat[0] + " " + lonlat[1] + ")', 4326)) < " + radius;
                    var sqlurl = "https://ogo.cartodb.com:443/api/v2/sql?format=GeoJSON&q=" + sqlquery;

                    var request = $.ajax({
                        url: sqlurl,
                        dataType: "json"
                    });

                    request.done(function (data) {
                        // we empty the message box
                        $("#info").html("");
                        
                        if (data.features.length > 0) {
                            // we just display the first hut in the array of features
                            var ft = data.features[0];
                            $("#info").append($("<p>").html(ft.properties.title));
                        } else {
                            // we display a default message in case of an empty array
                            $("#info").append($("<p>").html("No hut nearby, try elsewhere ..."));
                        }
                    });

                    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: 60px;
                background-color: white;
                border: solid gray 1px;
                padding: 5px;
                font-size: smaller;
                z-index: 1000000000;
            }

            p {
                margin: 0;
            }
        </style>
    </head>
    <body>
        <div id="map"></div>
        <div id="info"></div>
    </body>
</html>

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

  1. Ajouter un champ de saisie pour que l'utilisateur puisse contrôler le rayon de recherche (par défaut radius = 0.01° en dur dans le code ci-dessus)
  2. Plus le rayon est grand, plus la zone de recherche est vaste, trouvant ainsi plusieurs cabanes dans la zone. Adapter la solution pour que les informations de toutes les cabanes de la zone soient affichées (et pas seulement la première cabane)
geoinf18/oltuto4.txt · Last modified: 2019/01/08 17:15 by oertz