Synchronize Leaflet map data with AngularJS

AngularJS is a powerful tool for creating single page web applications, particularly data driven applications. One of the most powerful features of Angular is its two-way data binding, the automatic synchronization of data between the view and the application itself. In this example, we demonstrate the two-way data binding using a table and LeafletJS map of the 32 countries competing in the 2014 World Cup.

You can take a quick peak at the full app to get a sense of what is being described. The material below is intended to highlight some of the key code but is not a complete instruction manual for re-creating the map/table.

Leaflet maps in Angular

A key ingredient in Angular applications are directives, markers on web page elements that incorporate special behavior. Angular comes with a number of pre-made directives to help you build an application but programmers around the world have developed specialized directives to make it easier to incorporate, for example, file uploading, D3 charts, carousels and many other components. Most important to this post, though, are directives to include a LeafletJS map on our page. These directives, created by David Rubert, make it particularly easy to add a Leaflet map complete with legend, markers, zoom buttons etc. To see more about these directives he has created two different example pages here and here.

1. Initial setup

In order to use the Leaflet directives you will need to include three JavaScript files – the leaflet.js file from Leaflet, the angular.js file and the directive’s js file (angular-leaflet-directive.min.js). For the Leaflet and Angular JS files you can use a CDN, for the directive you can get it here. In our particular example we wanted to use Google tiles which requires an additional JS file, Google.js, which comes from here.

After you have the dependencies you’re ready to set up the Angular app. You first declare the app-level module and register the leaflet-directive using:

var myApp = angular.module('myApp', ['ngRoute', 'leaflet-directive']);

2. Data sources

We have two datasets we’re loading using $http.get() one with the country centroids (geojson) and one with the soccer rankings that we extracted from ESPN FC site (we have a blog post that explains the process). The centroids file was created using QGIS based on the World Borders Dataset provided by Bjørn Sandvik.

3. Create the table

We then linked the data to the table. The table linkage is straightforward and a common task in Angular. We simply create the table using the ng-repeat directive:

<!--     Psuedo-Code -->
<table ng-cloak class='table'>
   <thead>
      <tr class="foot">
         <th><a href="" ng-click="orderByField = 'country'; reverse=!reverse">Country</a>
         </th>
         <th><a href="" ng-click="orderByField = 'fifarank'; reverse=!reverse">FIFA Rank</a>
         </th>
      </tr>
   </thead>
   <tbody>
      <tr ng-repeat="foot in football | orderBy:orderByField:reverse | filter:search">
         <td class='country'>{{foot.country}}</td>
         <td>{{foot.fifarank}}</td>
      </tr>
   </tbody>
</table>

The table is linked to the country search text input box and the World Cup group pulldown through an object called search where search.country is connected to the text input and search.Group is connected to the group pulldown. (You might notice the ‘fix’ directive – this is used to make sure that Angular does not strip the value=“” from the empty option, see this fiddle).

<!--     Psuedo-Code -->

             <form role="form">
                      <input ng-model="search.country">
                      <select class="selectpicker" ng-model='search.Group' ng-options="city.Group as city.Group for city in football | unique:'Group' | orderBy:'Group'" fix>
                          <option value="" selected="selected">-- All Groups --</option>
                      </select>
                  </div>

              </form>

Here is what the table looks like (try it out):

4. Create the map

We create the map using the Leaflet directive. To do this you simply add a new element to your DOM (the web page). The majority of our page uses a controller called DemoController but in order to include the Google tiles we also wrap the Leaflet element in a div that uses the GoogleMapsController. So we have code that looks something like:

<!--     Psuedo-Code -->
<body ng-controller="DemoController">
   <div ng-controller='GoogleMapsController'>
      <leaflet center="world" events="events" legend="legend" width='100%' height='400' layers='layers' geojson="geojson"></leaflet>
   </div>
   <!-- ...Other Stuff... -->
</body>

Connecting the map data to the data in the table (filtered through the text box and pull down) is a little trickier. We set up a watchCollection which is used to watch both the text box and pulldown for changes and when changes are identified the full list of countries is whiddled down based on the filter values. The code for this is more complicated than necessary and I welcome comments on how it might be improved. A particular oddity in how I’ve handled this is that I’m using both the underscoreJS and the Angular filter functions for different tasks. This can definitely be done more succinctly.

        $scope.$watchCollection("search",
            function(newValue, oldValue) {

                if (newValue === oldValue) {
                    return;
                }
                var data = angular.copy($scope.footballgeo);

                var justGroup = _.filter(data.features, function(x) {
                    if (newValue.Group == '' || newValue.Group == undefined) {

                        if (!newValue.country) {
                            return true
                        } else {
                            return $filter('filter')([x.properties.country], newValue.country).length > 0
                        }
                    } else {
                        if (!newValue.country) {
                            return x.properties.Group == newValue.Group
                        } else {
                            return x.properties.Group == newValue.Group & $filter('filter')([x.properties.country], newValue.country).length > 0
                        }
                    }

                })

                data.features = justGroup
                $scope.geojson = {
                    data: data,
                    style: style,
                    resetStyleOnMouseout: true

                }

            }
        );

Here is what the map looks like (try it out):

5. Final touches, important notes and shortcuts taken

Adding graduated circles

I took a couple of shortcuts on this particular app. First of all, the Leaflet directive is easily set up to add markers to the map and polygon geojson but it may not be set up to add graduated circles (I could be wrong). So I made a slight adjustment to the directive to allow this. I changed this:

   geojson.options = {
                style: geojson.style,
                filter: geojson.filter,
                onEachFeature: onEachFeature,
                pointToLayer: geojson.pointToLayer
              };
              leafletGeoJSON = L.geoJson(geojson.data, geojson.options);

to this:

  leafletGeoJSON = L.geoJson(geojson.data, {
                style: geojson.style,
                filter: geojson.filter,
                onEachFeature: onEachFeature,
                pointToLayer: function (feature, latlng) {
                return L.circleMarker(latlng);
              }
              });

Cheat on legend

In the interest of time, I also cheated on the legend. The directive is nicely set up to add a traditional legend and the ‘Group’ part of the legend was created this way. But the graduated circle markers part of the legend is added manually using SVG. Not ideal.

Flags + Hover

The code for including the flags was taken directily from David Rubert’s example using polygons.

Click/Hover functionality

The click and hover functionality in the map is built in to the Leaflet directive and look like this:

$scope.$on("leafletDirectiveMap.geojsonMouseover", function(ev, leafletEvent) {
            //do something
        });

$scope.$on("leafletDirectiveMap.geojsonClick", function(ev, featureSelected, leafletEvent) {
                //do something
            });

Did you notice you can also click on the table. This takes advantage of Angular’s ng-click directive.

Take a look at the final result

2 responses

  1. How was the football.json file formatted? I looked at your linked blog post and saw that you grabbed the data from the URL but I don’t understand how you formatted the data afterwards. Thanks for any help.

    • It looks like this:

      [ { “alpha-3” : “ARG”, “country” : “Argentina”, “fifarank” : 7, “fifarating” : “1178.0”, “spirank” : 2, “spirating” : 90.2, “attack” : 2.9, “defense” : 0.4, “fifaspidiff” : 5, “fifaspiavg” : 4.5, “Group” : “F” },
      { “alpha-3” : “AUS”, “country” : “Australia”, “fifarank” : 59, “fifarating” : “545.0”, “spirank” : 40, “spirating” : 70.2, “attack” : 1.7, “defense” : 1.3, “fifaspidiff” : 19, “fifaspiavg” : 49.5, “Group” : “B” },
      { “alpha-3” : “BEL”, “country” : “Belgium”, “fifarank” : 12, “fifarating” : “1039.0”, “spirank” : 13, “spirating” : 80.9, “attack” : 2.1, “defense” : 0.8, “fifaspidiff” : -1, “fifaspiavg” : 12.5, “Group” : “H” }]

Leave a Reply to zev@zevross.com Cancel reply

Your email address will not be published. Required fields are marked *