Angular— Integrating with OpenLayers 3

Basic features to include mapping in your application

thedotisblack

Open source mapping libraries and OpenLayers 3

There are mainly two mapping libraries on the open source arena, Leaflet and OpenLayers. Personally I find OpenLayers 3, the latest version, to be the most complete and performant for complex GIS applications, but Leaflet can be very handy and good looking too.

For long lived projects I would recommend you to do some research and test based on your requirements before taking one or the other, both are great options. We will cover:

  • OpenLayer 3 brief introduction and map initialisation
  • Integrating OpenLayers with Angular

Source code available at gsans/ol3-angular (Github) or jump straight to the demo.

Follow me on Twitter for latest updates @gerardsans.

OpenLayers 3 Architecture

OpenLayers API is built on top of Google Closure library. You can look at it as the jQuery big brother.

The library uses an OO approach so you will be getting used to OpenLayers in-built types like ol.Map, ol.Collection, ol.layer.Vector, ol.source.KML, ol.Feature, etc. In order to operate with object properties we can use generic getters and setters. For example to read or set a feature property we can do

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

Handling Events

OpenLayers also offers a couple of APIs to subscribe and unsubscribe to object in-built events.

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

For example, if we wanted to subscribe to the click event on ol.Map we would do as follows

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

By using ol.Observable we can subscribe to any OpenLayers object property and even to new ones.

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

Integration with Angular

Angular integrates seamlessly with OpenLayers. There are two ways of interaction:

  • Map to View — this will be handled using the onFeatureSelected event handler
function onFeatureSelected(feature) {
// executes code in the next digest cycle
$timeout(function(){
vm.feature = feature;
selectTab("details");
});
}
  • View to Map — in our case we will be registering to a message bus to capture search filter changes and update the map;
// multipleFilter
$rootScope.$broadcast(“global.hide-features”, featuresArray);
// mainController
$rootScope.$on(“global.hide-features”, vm.hideFeatures);
function hideFeatures(event, features){
mapService.hideFeatures(features, vm.search);
}

from the search results selection, we will use ng-click to trigger feature selection on the map.

// 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);
}

OpenLayers setup

Include the corresponding header entries shown below. In order to render the map, you also need to specify a div with id equals to map.

The minimum CSS setup for styling our map involves setting a height and removing any padding or margins.

On the Angular side I will create a factory service to wrap all our mapping logic. The controller will kick off the map initialisation passing a config object.

In our example, the default blurry KML icons are replaced with crispy clean SVG icons from the new Google material library. As the icon alone looks flat I added a shadow effect using a composition of SVG filters.

To achieve the desired result I set extractStylesKml to false and changed popupOffset (in pixels) to match the icon with the final location.

Map initialisation

OpenLayers minimum initialisation requires you to setup a composition of different layers and a view. The main layer to include is the Tiles layer. This is the layer responsible for the actual map formed by tiles. You can use commercial (google maps, bing) or open source providers (open street maps).

The view is responsible for the projection used to position the different elements on the map. The target property will bind the map with the id used in <div id=”map”></div>.

I am transforming latitude and longitude coordinates to Spherical Mercator (EPSG:3857). Latitude ranges from -90 (south pole) to a maximum of 90 (north pole). Longitude ranges from -180 (west of the prime meridian) to 180 (east of the prime meridian).

OpenLayers can parse different data formats (GeoJSON, GPX, KML). I am going to use KML as it is the default format used by Google Earth and easily available exporting any Google map created online.

OpenLayers object model for vector sources looks like:

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

MapService interactions

MapService will implement these interactions:

  • search filtering — triggered when the users changes the search query, hides features not matching the search criteria
  • results feature selection — triggered when the user selects a result, using the feature id we tell the map to pan and center the selected feature
  • map feature selection — triggered when the user clicks on a marker, displays the feature details and selects the details tab

In order to support these features I will use (same order):

KML Features Navigation

In order to navigate through the KML features I have implemented a search feature and a details view. You can enter your search query (allows multiple terms) and from the results you can navigate to the feature location on the map. On the details view you will see the feature details and if a query matches any content it will get highlighted.

For the UI I have used UI Bootstrap from the Angular UI Team (Bootstrap 3). In order to style the tabs I have used search-tab and details-tab CSS classes.

From the code above, I am using the active attribute on the tab element to dynamically change the selected tab. You can then activate a tab like:

$scope.staticTabs.search = true;

I implemented a helper function as

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

Maybe you had already noticed from the code above that I did not set the other tabs to false. That is because Angular UI tab controller already takes care of that.

I implemented two custom filters: multiple and highlight; to be able to query using multiple terms and highlighting those terms on the details tab. Filters are respectively adaptions from and-filter and highlight.

Search tab

In order to implement the search I have used common Angular directives like ng-show, ng-repeat and ng-click.

From the code above notice

  • Input debounce setting using ng-model-options — this is to add a small delay after you type on the search query.
  • Alias expression for ng-repeat — this stores the temporary results for the filtered expression.
f in features | multiple: search: this as filtered
f in filtered = (features | multiple: search: this)

You can read more about how to pass parameters, chain and compose filters on this post.

Details tab

This tab is pretty straightforward using standard Angular features. I am mapping the different bits of information to the feature object.

From the code above:

  • Render unsafe content with ng-bind-html— As we display the information coming from the KML source we can protect ourselves sanitizing this content.
  • Highlight filter — this will highlight the search terms over the content.
In order to use ng-bind-html I have included the dependency ngSanitize to the application and included the corresponding angular-sanitize.js file to index.html.

Thanks for reading! Have any questions? Ping me on Twitter @gerardsans.

Resources

OpenLayers 3 Beginner’s Guide, OpenLayers Workshop, Examples from The book of OpenLayers 3, angular-openlayers-directive, Library combining OpenLayers 3 and AngularJS (ngeo)