Visualizing Geographic Data Using AngularJS and Mapbox
Building map views of your data without the performance issues by taking advantage of PruneCluster.
Join the DZone community and get the full member experience.
Join For FreeOne of the best things when working with geographic datasets is that you get to play around with maps. For all of my interactive visualizations, I have been using LeafletJS to achieve this on web browsers, but recently I hit some performance issues while doing this, along with AngularJS. This article describes the technology stack I used to build my application, along with how I got around some performance issues.
The Basics
For any web application that I build, I always turn to NodeJS, so first make sure you have that installed on your machine. Next, to get started really quickly, I'm going to use Yeoman to use a generator to build up the basics of an application. Install this globally using npm:
npm install -g yo
Yeoman is a web scaffolding framework that takes a lot of the pain out of starting a web application; all that boring boilerplate stuff that you do every time. Having said that, it's not for everyone, as it can bring along a lot of dependencies and code you might not use. For our purposes, Yeoman is fine.
You will also need to install Grunt, a JavaScript task manager and Bower, for managing client side dependencies.
npm install -g grunt-cli bower
Next, we'll use a generator that provides us with Express and AngularJS, called generator-angular-fullstack.
npm install -g generator-angular-fullstack
Now we have everything we need installed, go to a new directory to contain your project and use the generator to build a new application. On the command line type:
yo angular-fullstack maps
In the above, I used maps as the application name. All you need to do now is answer the questions to suit your project. For this example MongoDB is not necessary, but I have used Bootstrap for my UI. Again, this is up to you, and not relevant for our map example.
You can run your application by using the following command with grunt:
grunt serve
The application runs on port 9000 by default, and will look as follows
Creating an API endpoint to retrieve your data
The next thing that you should do is add an endpoint within your application that will return your geographic data. Creating the endpoint with the generator is trivial. All you need to do is provide the name of the endpoint, in our case, locations:
yo angular-fullstack:endpoint location
If you point the browser to http://localhost:9000/api/locations, you will get an empty array back. Let's fill that with our location data.
The endpoint's controller is located server/api/location/location.controller.js, and you can see that it just returns an empty array as a response:
'use strict';
var _ = require('lodash');
// Get list of locations
exports.index = function(req, res) {
res.json([]);
};
What we need to do now is read pass back our real location data. Ideally you will have your own data, but for this example, I went to the Stanford Network Analysis Project and took some of their (anonymized) Gowalla location data. This provides a large set of longitude and latitude points to attempt to visualize on a map, but rather than take the entire file I just copied the first 5000 records.
As we are reading the file in our NodeJS server, the easiest thing to do is place the file in the root directory. Next, install the CSV-streamer package using npm to provide us with the ability to read the file and convert it into JSON objects. Note that we are using --save here so that the package information is saved to package.json.
npm install csv-streamer --save
Your controller now looks as follows, taking the latitude and longitude from the tab separated file in indexes 2 and 3, returning the data once you read 4999 lines:
var _ = require('lodash');
var fs = require('fs');
var CSVSteam = require('csv-streamer');
// Get list of locations
exports.index = function(req, res) {
var csv = new CSVSteam({headers: false, delimiter:'\t'});
var count = 0;
var data = [];
csv.on('data', function(line){
data.push({lat: line[2], lon: line[3]});
if(data.length == 4999){
res.json(data);
}
});
fs.createReadStream('locs.txt').pipe(csv);
};
With our API returning location data, it's time to get to the real work.
Using Leaflet with AngularJS
Let's get started by displaying a simple map in our AngularJS application. To do this with a minimum of fuss, we'll use the angular-leaflet-directive.
First, install the package using bower, and save it to your bower.json file:
bower install angular-leaflet-directive --save
I also installed the leaflet-plugins package:
bower install leaflet-plugins --save
Restart the grunt process using grunt serve to ensure that the dependency is loaded. You can double check this by opening index.html and checking that the scripts are loaded as follows at the end of the file:
<script src="bower_components/leaflet/dist/leaflet-src.js"></script>
<script src="bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.js"></script>
For more familiar maps, it's a good idea to include the Google Maps API here too, but outside of the bower sections of your HTML
<script type="text/javascript" src=
"https://maps.googleapis.com/maps/api/js?libraries=geometry">
<!--[if lt IE 9]>
<script src="bower_components/es5-shim/es5-shim.js"></script>
<script src="bower_components/json3/lib/json3.min.js"></script>
<![endif]-->
<!-- build:js({client,node_modules}) app/vendor.js -->
<!-- bower:js -->
Add the dependency on the leaflet directive to your app.js file, which defines the Angular module:
angular.module('mapsApp', [
'ngCookies',
'ngResource',
'ngSanitize',
'ui.router',
'ui.bootstrap',
'leaflet-directive'
])
Next, move to the Angular controller for the main page, under client/app/main.controller.js. Here you can set some defaults for the map that will be displayed.
angular.module('mapsApp')
.controller('MainCtrl', function($scope, $http) {
angular.extend($scope, {
center: {
lat: 37.7532511,
lng: -122.4512832,
zoom: 3
},
defaults: {
scrollWheelZoom: false
},
events: {
map: {
enable: ['zoomstart', 'drag', 'click', 'mousemove'],
logic: 'emit'
}
},
layers: {
baselayers: {
googleRoadmap: {
name: 'Google Streets',
layerType: 'ROADMAP',
type: 'google',
"layerOptions": {
"showOnSelector": false
},
}
}
},
markers: {}
});
The code above simply sets up the map to use the Google Streets base layer and sets the center point to be San Francisco. Now, to have a simple map displayed we can add the directive to main.html
<div class="container">
<div class="row">
<div class="col-lg-12">
<leaflet id="mymap" defaults="defaults" markers="markers" center="center" height="600px" layers="layers" width="100%"></leaflet>
</div>
</div>
</div>
The application is finally taking shape!
Display Location Markers
Now we need to make a call to our API to retrieve the markers and display them on our map. You can do this in your AngularJS controller with minimum fuss, first adding a data attribute to our scope so it can be shared around the page.
$scope.data = null;
$http.get('/api/locations').success(function(data) {
$scope.data = data;
});
To display the marker clusters, you might consider using the standard marker cluster solution for Leaflet (Leaflet.MarkerCluster), which is well supported by the angular-directive. However, as this example shows, this can be a little slow when you're dealing with a large amount of markers. I hit such a limitation, but found a few recommendations for PruneCluster which performs much better when faced with a large amount of markers. The 10,000 marker test runs very fast with this library.
The problem is that there's no 'native' support for PruneCluster in the AngularJS leaflet directive, so you'll need to do a few things differently to get it working.
Integrating PruneCluster
First, you'll need to download the PruneCluster dependency using bower
bower install PruneCluster --save
Bower wouldn't automatically add the library to my HTML, so I had to do it manually. The CSS stylesheet for PruneCluster will need to be included on index.html
<!-- endbower -->
<!-- endbuild -->
<!-- build:css({.tmp,client}) app/app.css -->
<link rel="stylesheet" href="bower_components/PruneCluster/dist/LeafletStyleSheet.css" />
<link rel="stylesheet" href="app/app.css">
<!-- injector:css -->
And the JavaScript needs to be included at the end of the same file
<!-- endbuild -->
<script src="bower_components/PruneCluster/dist/PruneCluster.js"></script>
<!-- build:js({.tmp,client}) app/app.js -->
Now, because you don't have access to PruneCluster in our directive, you'll need to get direct access to the Leaflet Map object. You can do this by adding a dependency on the leafletData service on your controller
angular.module('mapsApp')
.controller('MainCtrl', function($scope, $http, leafletData) {
When the map loads, the leafletData.getMap function will be executed. When that happens you can store a reference to the map in your controller's scope
$scope.map = null;
//called when the map is loaded
leafletData.getMap("mymap").then(function(map) {
$scope.map = map;
});
With access to the Map object you can do whatever you want with Leaflet. Let's finish by using PruneCluster to render the markers.
The renderMarkers function creates a layer with PruneCluster, registers each marker individually, and processes the view once it has all of that completed:
/**
* Render the markers onto the map
* @param {Array} data the geographic data to be displayed
* @param {Leaflet.Map} map the map to render the markers on
*/
function renderMarkers(data, map){
//create layer for the markers
var markerLayer = new PruneClusterForLeaflet();
for(var i =0 ; i < data.length; i++){
var marker = new PruneCluster.Marker(data[i].lat, data[i].lon);
markerLayer.RegisterMarker(marker);
}
//add the layer to the map
map.addLayer(markerLayer);
//need to be called when any changes to markers are made
markerLayer.ProcessView();
};
Now when we retrieve the geographic data through the API, we can simply call the renderMarkers function
$http.get('/api/locations').success(function(data) {
$scope.data = data;
renderMarkers(data, $scope.map);
});
Which results in the markers appearing really quickly.
You will find many more examples of how to use PruneCluster on the GitHub page, including the ability to use custom markers, filtering and lots more.
Opinions expressed by DZone contributors are their own.
Comments