How I Found A Job With Node + Angular, Part 5: Mind The Map

In this post I would like to start adding some mapping functionality to our application. As always, there are a lot of different ways to achieve this goal, but I have chosen to go with Google Maps as the mapping solution provider, because of their clean and simple API, the visualization layers, and frankly – because I have used it in the past and know it a little bit. Even with that decided – there still a lot of ways to incorporate the Google Map in our application. Let’s try an Angular add-on that will aBowerdd Google Maps directives to Angular. It is called Google Maps for AngularJS, and before we can use it, let’s install another package and dependency manager that will ease the add-on installation – Bower. You can install it yourself by placing the add-on JS in your js sub folder, but you will need to install some dependencies as well (lodash in this case). Bower packages will be placed under the “bower_components” sub folder.

> npm install -g bower
...
> bower install angular-google-maps

After the packages are installed, you can reference them directly from the install folders:
bower-packages

In my tutorial I copied the relevant files to the JS/ sub folder, and added the following to the html file (lines 3-5):

...
  <script type="text/javascript" src="js/angular.min.js"></script>
  <script type="text/javascript" src='//maps.googleapis.com/maps/api/js?sensor=false'></script>
  <script type="text/javascript" src='js/lodash.underscore.min.js'></script>
  <script type="text/javascript" src='js/angular-google-maps.min.js'></script>
  <script type="text/javascript" src="js/geojob-app.js"></script>
</body>
</html>

Now we are ready to use the Google Maps API in our application.

First, let’s add the map to our controller:

   app.controller('JobsController', function($scope, $http) {
      $scope.hello = "Hello GeoJob";
      $scope.jobs = {};
      $scope.company = {};
      $scope.map = {
        draggable: true,
        center: {
            latitude: 32.1092255,
            longitude: 34.838992
         },
         zoom: 17
      };
...

lines 5-12 define the map variable (object) with some predefined initial values. We will make the map initialization dynamically in the next posts, but for now let’s try to use this map. I decided to draw a circle around each company location which is proportional to the number of employees that work at that company. Let’s add the circle variable with some predefined initial values:

   app.controller('JobsController', function($scope, $http) {
...
      $scope.circle = {
         center: $scope.map.center,
         radius: 50,
         stroke: {
           color: '#B2081F',
           weight: 1,
           opacity: 1
         },
         fill: {
           color: '#B2081F',
           opacity: 0.5
         },
         geodesic: true, // optional: defaults to false
         draggable: false, // optional: defaults to false
         clickable: false, // optional: defaults to true
         editable: false, // optional: defaults to false
         visible: true // optional: defaults to true
      };
...

Here we can see how the circle border (stroke) and fill colors are defined, along with some other circle properties. The circle is centered (by default) to the map center and has a default radius of 50 meters.

I would also like to mark each company location by a map marker, which will give some details about the company. Let’s start simple by using the default marker, and showing the company name when the marker is clicked. For this let’s define the marker events:

   app.controller('JobsController', function($scope, $http) {
...
      $scope.markersEvents = {
        click: function (marker, eventName, model) {
          marker.showWindow = true;
        }
      };
...

The markerEvents variable defines the array of events and their callback functions. For now we defined only the click event and the function it calls will open the marker info window (the info window will inherit the default window behavior which include an “X” close button and will display the info window html fragment – we will see how it is initialized later in the html file). Finally, we need to calculate the radius of the circle from the company employees number. I decided to divide it by 10 for simplicity (so 500 employees company should draw a 50 meter radius circle on the map). Here is the controller, with lines 56-58 showing the radius calculation:

(function() {

   var app = angular.module('geojob', ['google-maps']);

   app.controller('JobsController', function($scope, $http) {
      $scope.hello = "Hello GeoJob";
      $scope.jobs = {};
      $scope.company = {};
      $scope.map = {
        draggable: true,
        center: {
            latitude: 32.1092255,
            longitude: 34.838992
         },
         zoom: 17
      };
      $scope.circle = {
         center: $scope.map.center,
         radius: 50,
         stroke: {
           color: '#B2081F',
           weight: 1,
           opacity: 1
         },
         fill: {
           color: '#B2081F',
           opacity: 0.5
         },
         geodesic: true, // optional: defaults to false
         draggable: false, // optional: defaults to false
         clickable: false, // optional: defaults to true
         editable: false, // optional: defaults to false
         visible: true // optional: defaults to true
      };
      $scope.markersEvents = {
        click: function (marker, eventName, model) {
          marker.showWindow = true;
        }
      };

      $http.get("/jobs").success(function(data) {
         $scope.jobs = data;
      }).error(function() {
         alert("Error");
      });

      $scope.setCompany = function(company) {
        $scope.company = company;
      };

      $scope.isSelected = function(company) {
        return $scope.company == company;
      };

      $scope.getRadius = function(job) {
      	return job.value.employees / 10;
      };

   });

})();

line 3 also show how we added the ‘google-map’ module dependency to our ‘geojob’ module.

Now we can use the modified controller in our html file:

<html ng-app="geojob">
<head>
  <title>GeoJob Finder - Bootstrap GeoJob</title>
  <link rel="stylesheet" href="bootstrap-3.1.1-dist/css/bootstrap.min.css" />
  <link rel="stylesheet" href="bootstrap-3.1.1-dist/css/bootstrap-theme.min.css" />
  <link rel="stylesheet" href="css/geojob.css" />
</head>
<body>
  <section class="container"  ng-controller="JobsController">
    <div class="header"><h1>GeoJob Finder <small>Bootstrap GeoJob</small></h1></div>
    <div class="well well-sm">We have a total of: {{jobs.total_rows}} jobs</div>
    <div class="list-group companylist">
      <a href="#" class="list-group-item" ng-class="{active : isSelected(job)}" ng-repeat="job in jobs.rows" ng-click="setCompany(job)">{{job.key}}</a>
    </div>
    <google-map center="map.center" zoom="map.zoom" draggable="{{map.draggable}}">
      <div ng-repeat="job in filtered = (jobs.rows)">
    	<marker coords="{latitude: job.value.lat, longitude: job.value.lon}" events="markersEvents" showWindow="false">
    	  <window>
    	  <p>{{job.key}}</p>
    	  </window>
    	</marker>
        <circle center="{latitude: job.value.lat, longitude: job.value.lon}" stroke="circle.stroke" fill="circle.fill" radius="getRadius(job)"
           visible="circle.visible" geodesic="circle.geodesic" editable="circle.editable" draggable="circle.draggable" clickable="circle.clickable"></circle>
      </div>
    </google-map>
    <div class="panel panel-default companydetails">
      <div class="panel-body">
        Name: <span style="float: right">{{company.key}}</span><br>
        <span style="float: right">{{company.value.name}}</span><br>
        Sector: <span style="float: right">{{company.value.sector}}</span><br>
        Employees: <span style="float: right">{{company.value.employees}}</span><br>
        Area: <span style="float: right">{{company.value.area}}</span><br>
        Location Name: <span style="float: right">{{company.value.locationName}}</span><br>
        Location Area: <span style="float: right">{{company.value.locationArea}}</span><br>
      </div>
    </div>
  </section>
  <script type="text/javascript" src="js/angular.min.js"></script>
  <script type="text/javascript" src='//maps.googleapis.com/maps/api/js?sensor=false'></script>
  <script type="text/javascript" src='js/lodash.underscore.min.js'></script>
  <script type="text/javascript" src='js/angular-google-maps.min.js'></script>
  <script type="text/javascript" src="js/geojob-app.js"></script>
</body>
</html>

Lines 15-25 show the changes we made to the html file. It is the Angular Google Map directive that wraps the markers and circles directives.
Line 15 – the directive initializes the map with values from the $map variable in the controller.
Line 16 is Angular repeater for each job in the filtered job list. For now we will iterate all the jobs in the list, but we will add some filters later on to filter specific jobs from this list.
Lines 17-21 add the marker. Note how the info window is initialized with the company name ({{job.key}}) and the marker is placed in the company location (coords=”{latitude: job.value.lat, longitude: job.value.lon}”) with the info window closed by default (showWindow=”false”). The marker events are set to the controller markerEvents we’ve seen above.
Lines 22-23 add the circle. The circle center and radius are set to the company location (center=”{latitude: job.value.lat, longitude: job.value.lon}”) and calculated from the company employees number (radius=”getRadius(job)”), with other values set to the controller circle variable values.

The result? Here is a sample screen from our jobs db:google-map-sample1

You can see the map, markers and circles. One marker was clicked to open it’s info window. The circles are proportional to the company sizes (employees), and the selected company circle radius is 50 meters according to it’s number of employees.

In the next post we will enhance the mapping functionality and add some cool filtering features.

Note: As I write this post, a rocket from Gaza destroyed a house 500 meters from my home. Please support Israel in it’s battle against the Hamas terror organization.

In the previous post I promised to tell you how to fix the mingled Hebrew characters display. As it turned out, this was a bug in the way I exported the company information from my excel. My bad! Couch DB support UTF-8 by default, so once the information was exported correctly – the problem disappeared.

If the phrase “Mind The Map” sound familiar to you, this is probably because it is a paraphrase on the famous “Mind The Gap” phrase.

How I Found A Job With Node + Angular tutorial series:

Part 1: Let’s Do Some Node
Part 2: Let’s Level Some Jobs
Part 3: The Angular Angle
Part 4: Strap The Boot
Part 5: Mind The Map
Part 6: Going Production

Leave a Reply

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