Usando OpenLayers 3 con AngularJS

Dashboarde de BI con QlikView

AngularJS y OpenLayers 3 para crear un mapa de rutas de montaña

 

Google domina totalmente en el area de mapas por encima de cualquier otra opcion. El problema principal de usar Google maps es la limitacion de uso para soluciones internas o para mantener una API propia.

En estos momentos hay dos librerías open source para mapas que destacan sobre las otras. Estas son:

Personalmente encuentro OpenLayers 3, la versión mas reciente de la librería, la mas completa y con mejor rendimiento para productos GIS, igualmente Leaflet es muy fácil de usar y sexy.

Para proyectos medianos o grandes recomendaría hacer un estudio de las opciones disponibles según los requerimientos del proyecto. En cualquier caso las dos opciones son realmente buenas. En este articulo voy a cubrir los siguientes puntos:

  • Breve introducción a OpenLayer 3 y la inicialización del mapa
  • Como integrar el mapa con Angular

El código final esta disponible en gsans/ol3-angular (Github), sino podéis ir directamente a la demo online.

Arquitectura de OpenLayers 3

La API de OpenLayers esta basada en la librería Google Closure. Esta librería es bastante mayor que jQuery asi que es bastante interesante para proyectos medianos o grandes.

OpenLayers usa el paradigma de orientación a objetos. Es importante familiarizarse con los tipos básicos como ol.Map, ol.Collection, ol.layer.Vector, ol.source.KML y ol.Feature. Para acceder a las propiedades de cualquier objeto usaremos los métodos get y set. Por ejemplo para leer o cambiar el valor de una propiedad del objeto ol.Feature usaremos

olFeature.get(key);
olFeature.set(key, value);

Eventos nativos de OpenLayers

OpenLayers ofrece una API especifica para suscribirnos y cancelar una subscripción a los eventos propios de un objeto.

on(type, listener, opt_this)  // subscribe
once(type, listener, opt_this) // subscribe only once
un(type, listener, opt_this) // unsubscribe

Por ejemplo, si quisieramos suscribirnos al evento click de ol.Map haríamos

olMap.on('click', function(event) {...});

Usando la clase abstracta ol.Observable nos permite suscribirnos a propiedades predefinidas de cualquier objeto en OpenLayers e incluso a propiedades que creemos nuevas.

olMap.on('change:size', function(event) {...}); // standard API
olMap.set('myNewProperty', true);  // for a new property
olMap.on('change:myNewProperty', function(event) {...});

Integracion con Angular

Angular se integra perfectamente con OpenLayers. Hay dos formas basicas de interaccion:

  • Interacciones iniciadas en el mapa que modifican la vista de Angular — estas seran procesadas por el evento delegado onFeatureSelected
function onFeatureSelected(feature) {
  // executes code in the next digest cycle 
  $timeout(function(){      
    vm.feature = feature;      
    selectTab("details");
  });
}
  • Interacciones iniciadas desde la vista que modifican el mapa — en nuestro caso usaremos un bus de mensajes para capturar los resultados de la busqueda y filtrar asi los marcadores visibles en el mapa;
// multipleFilter
$rootScope.$broadcast(“global.hide-features”, featuresArray); 
// mainController
$rootScope.$on(“global.hide-features”, vm.hideFeatures);
function hideFeatures(event, features){
  mapService.hideFeatures(features, vm.search);
}

para los resultados de la busqueda usaremos el evento ng-click para seleccionar el marcador correspondiente en el mapa.

// index.html 
<li ng-repeat=”f in filtered = (mc.features | multiple: mc.search)”
  class=”feature-result” ng-click=”mc.selectFeature(f.name);”>
  <div><span>{{f.name}}</span></div>
</li>
// mainController.js
function selectFeature(featureId){
  mapService.selectFeature(featureId, panToFeature);
}

Configuracion de OpenLayers

Para usar OpenLayers debemos incluir las siguientes entradas en la cabecera de nuestra pagina HTML como podemos ver en el siguiente código. Para renderizar el mapa debemos crear ademas un elemento div con id igual a map.

<head>
  ...
  <script src="http://ol3js.org/en/master/build/ol.js"></script> 
  <!-- Include this version to improve your development/debugging experience
  <script src="http://ol3js.org/en/master/build/ol-debug.js"></script> 
  -->
  <link rel="stylesheet" href="http://ol3js.org/en/master/css/ol.css" />
  ...
</head>
<body>
  ... <div id="map"></div>
</body>

Para empezar a usar el mapa necesitamos dar un valor a la propiedad height en los estilos CSS del mapa. Adicionalmente si queremos ocupar toda la pagina, tendremos que inicializar las propiedades padding y margin.

html, body, #map {
  padding: 0;
  margin: 0;
}
 
#map {
  width: 100%;
  height: 200px;
}

En la aplicacion de Angular, vamos a definir un controlador para nuestra vista y en el vamos a crear un servicio tipo factory para encapsular toda la logica del mapa. El controlador se encargara de inicializar el mapa pasando un objeto de configuracion.

app.controller('MainController', function($scope, mapService) {
  mapService.init({
    extractStylesKml: false,
    popupOffset: [-4,-43],
    featurePropertiesMap: ['name', 'description', 'Enlace a la ruta'], //override default mapping
    onFeatureSelected: onFeatureSelected //override default event handler
  });
  $scope.features = mapService.getFeatures();
...
});
En nuestra demo hemos substituido los iconos por defecto de KML por iconos SVG de la librería Google material. Como los iconos SVG aparecen sin volumen hemos incluido un pequeño efecto de sombra usando una composicion de varios filtros SVG. Esto ademas de mejorar la calidad de visualizacion, tambien evita que cuando varios marcadores estan muy cerca no se puedan diferenciar.

Para ajustar el mapa al icono SVG escogido vamos a pasar la configuracion extractStylesKml a false y popupOffset (en pixels) para hacer coincidir el marcador con las coordenadas del mapa.

Inicialización del mapa

La inicialización del mapa normalmente esta formada por varias capas y una vista principal. La capa mas importante es la encargada de mostrar la informacion visual del mapa en pequeñas cuadriculas (tiles). OpenLayers permite multiples proveedores comerciales (Google maps, Bing) o de código libre (Open Street Maps, MapBox, Stamen).

La vista (view) es responsable de la proyeccion, esta define las posiciones de los diferentes elementos del mapa. La propiedad target nos permitira enlazar el mapa con el id usado en <div id=”map”></div>.

// map initialisation
ms.map = new ol.Map({
  target: 'map',
  layers: [
    new ol.layer.Tile({
      source: new ol.source.MapQuest({layer: 'osm'})
    })
  ],
  view: new ol.View({
    center: ol.proj.transform(
        [0, 40], 'EPSG:4326', 'EPSG:3857'),
    zoom: 5
  })
});
En la vista estamos transformando las coordenadas expresadas con latitud y longitud (EPSG:4326) a coordenadas Spherical Mercator (EPSG:3857).

La coordenada de Latitud va desde -90 (polo sur) hasta un maximo de 90 (polo norte). La coordenada Longitud va desde -180 (oeste del meridiano primario) hasta 180 (este del meridiano primario).

OpenLayers puede leer multiples formatos (GeoJSON, GPX, KML). Vamos a usar KML por ser el mas popular (Google Earth, foursquare, etc.), y tambien por ser fácil de conseguir desde mapas creados online.

El modelo de objetos para OpenLayers para origenes de datos vectoriales se podria resumir en:

map -> layers -> sources -> features -> geometries

Interacciones con MapService

El servicio MapService va a implementar estas interacciones:

  • filtro de busqueda — cada vez que el usuario cambia los terminos de la busqueda, los marcadores no incluidos en los resultados deben ocultarse del mapa
  • seleccion de un resultado de la busqueda — cada vez que un usuario selecciona un resultado de la lista, se debe seleccionar el marcador en el mapa y mostrar los detalles
  • seleccion de un marcador en el mapa — cada vez que el usuario selecciona un marcador, se centra en el mapa y se muestran un popup y activando la seccion de detalles

Para dar soporte a estas funcionalidades vamos a usar (en el mismo orden):

mapService.hideFeatures(filteredFeatures:Array, searchQuery:string);
mapService.selectFeature(featureId:string, panToFeature:boolean);
function onFeatureSelected(feature:object) {...}

Navegación de marcadores KML

Para navegar los marcadores (features) del origen KML hemos implementado una seccion de busqueda y otra de detalles.

Para buscar se permiten varios terminos y desde la lista de resultados podemos navegar a la posicion del marcador en el mapa. En la seccion de detalles podemos ver el contenido del marcador tal como aparecen en el origen KML. Para facilitar la visualizacion de la busqueda se resaltan los terminos encontrados sobre el mismo contenido en amarillo.

Para la presentacion hemos usado la librería de UI Bootstrap que usa internamente los estilos de Bootstrap. Para separar los estilos de las secciones hemos creado las clases CSS search-tab y details-tab.

<tabset>
  <tab heading="Search" active="staticTabs.search">
    <div class="search-tab">
    </div>
  </tab>
  <tab heading="Details" active="staticTabs.details">
    <div class="span4 offset4 details-tab">
    </div>
  </tab>
</tabset>

Usamos el atributo active del elemento tab para seleccionar la seccion desde el código. Podemos asi activar una seccion simplemente con:

$scope.staticTabs.search = true;

Para seleccionar una seccion podemos usar esta funcion de ayuda pasando el identificador

function selectTab(key){
   if ($scope.staticTabs.hasOwnProperty(key))
     $scope.staticTabs[key] = true;
}

Del código anterior, se puede apreciar como no necesitamos usar ninguna logica para inicializar las secciones del elemento tab a false. Podemos ahorrarnos esta logica pues el controlador de Angular UI tab se encarga de hacerlo por nosotros.

Hemos implementado dos filtros a medida: multiple y highlight; para poder usar multiples terminos de busqueda y resaltar los terminos en la seccion de detalles. Los filtros originales usados como base son adaptaciones de and-filter y highlight.

Sección para la búsqueda

Busqueda openlayers jaangular mapa de rutas

Click aquí o sobre el mapa para acceder a la demo interactiva

Para implementar la busqueda hemos usado las directivas nativas de Angular: ng-show, ng-repeat y ng-click.

<div class="search-tab">
  <div class="input-group">
    <span class="input-group-addon"><ng-md-icon icon="search" style="fill: #000;" size="18"></ng-md-icon></span>
    <input type="text" class="form-control" ng-model="search" ng-model-options="{ debounce: 1000 }" aria-describedby="inputGroupSuccess1Status">
    <div class="input-group-addon">
      <span ng-show="features.length>0"><span>{{filtered.length}}</span> Result/s</span>
    </div>
  </div>
  
  <div class="list" ng-show="filtered.length>0">
    <ul>
      <li ng-repeat="f in filtered = (features | multiple: search: this)" class="feature-result" ng-click="selectFeature(f.name);">
        <div>
          <span>{{f.name}}</span>
        </div>
      </li>
    </ul>
  </div>
</div>

Del código anterior vamos a resaltar

  • la configuracion debounce del elemento input usando ng-model-options— esto nos permite añadir un pequeño retraso cuando introducimos nuestra busqueda para no colapsar el mapa con cambios constantes.
  • la expresion usando un alias para ng-repeat — esto crea una variable temporal para mantener los resultados temporales de nuestro filtro.
f in features | multiple: search: this as filtered
f in filtered = (features | multiple: search: this)

Se puede encontrar informacion mas detallada sobre filtros (paso de parametros, encadenar multiples filtros y como usar composicion) en este otro post.

Sección Detalles

Informacion openlayers jsangular mapa de rutas

Click aquí o sobre el mapa para acceder a la demo interactiva

Esta seccion es muy fácil de implementar con Angular usando las directivas estandard. Para mostrar la informacion estamos mapeando directamente el objeto feature en el controlador.

<div class="span4 offset4 details-tab">
  <div ng-show="feature">
    <h2><a href="{{feature['Enlace a la ruta']}}" target="_blank"><span ng-bind-html="feature.name | highlight:search"></span></a></h2>
    <p ng-bind-html="feature.description | highlight:search"></p>
  </div>
  <div ng-show="!feature">
    <h2>No Details available.</h2>
    <p>Select a marker on the map to see the details.</p>
  </div>
</div>

Sobre el código anterior quiero comentar:

  • Como incluir contenido HTML con ng-bind-html— este contenido es potencialmente inseguro pues viene del archivo KML, para protegernos de código malicioso debemos sanear el código con ngSanitize.
  • Filtro para resaltar con Highlight — con este filtro vamos a resaltar los terminos de la busqueda en amarillo.

Para usar ng-bind-html debemos incluir ngSanitize en nuestra aplicacion y tambien añadir el archivo angular-sanitize.js a vuestra pagina.

Recursos

Demo online del mapa con AngularJS y OpenLayers 3

 

OpenLayers Workshop, Ejemplos online del libro OpenLayers 3

 

AngularJs Meetup South London Collection | this article

 

 

Muy buen artículo, me ha sido muy útil, me gustaría saber si se podría usar en vez de formato kml con formato GeoJSON a través de una consulta wfs.