//
you're reading...

AngularJS

Part 5: Web application development using AngularJS, Spring Boot and Maven – Support for eBird APIs, HTML 5 Geolocation and Google Geocoding

In my previous post, I completed the first phase of my Bird Log application. I can use this application to view my current sightings and add new sightings. I also implemented very basic support for content filtering. In this post, I am going to use eBird APIs to display current bird sightings reported on ebird.org, near my current location. I plan to use HTML5 Geolocation APIs to find out my current location automatically and then use Google Geocoding APIs to map my current Geographic coordinates (latitude and longitude) to a human-readable address.### Integrate with ebird.org using ebird APIs
– Create a new file <project folder>/src/main/resources/static/app/js/controllers/ebird.js
– Create a new module called starterApp.ebird. This module depends on ngResource module.

        angular.module("starterApp.ebird", ["ngResource"])
- Add eBirdApiFactory that uses `$resource` to load recent bird sightings from `eBird.org`

        .factory('eBirdApiFactory', function($resource) {
            return $resource('http://ebird.org/ws1.1/data/obs/geo/recent?fmt=json', 
                             {lat: 42.3581, lng: -71.0636, dist: 2, back: 5, maxResults: 500, locale:'en_US'});
        })

    **NOTE**: 
    - This will send the following HTTP request:
              `http://ebird.org/ws1.1/data/obs/geo/recent?fmt=json&back=5&dist=2&lat=42.3581&lng=-71.0636&locale=en_US&maxResults=500`
    - **Default parameter values** specified above can be over written when the query method is called.
- Implement `ebirdApiCtrl` that uses `eBirdApiFactory` to populate the `$scope` field called `recentSightings`.

        .controller("ebirdApiCtrl", ['eBirdApiFactory', 
                                        function(eBirdApiFactory) {
            $scope.recentSightings = [];
            eBirdApiFactory.query(function(data){
                $scope.recentSightings = data;
            });
        }]);

  **Note**: Every time `ebirdApiCtrl` is loaded, `eBirdApiFactory.query` is executed and recent sightings are retrieved from the ebird.org server.
  • Create ebird_sightings.html to display recent bird sightings in a tabular view. Use ng-repeat directive to generate the table.
    <h3>Recent Bird Sightings in Boston Area</h3>
    <div class="table-responsive">
        <table class="table table-striped">
            <thead>
                <tr>
                    <td>Common Name</td>
                    <td>Scientific Name</td>
                    <td>Observed On</td>
                    <td>Count</td>
                    <td>Location</td>
                    <td>Is Private?</td>
                    <td>Reviewed?</td>
                    <td>Valid?</td>
                </tr>   
            </thead>
            <tbody>
                <tr ng-repeat="sighting in recentSightings">
                    <td>{{sighting.comName}}</td>
                    <td>{{sighting.sciName}}</td>
                    <td>{{sighting.obsDt}}</td>
                    <td>{{sighting.howMany}}</td>
                    <td>{{sighting.locName}}</td>
                    <td>{{sighting.locationPrivate}}</td>
                    <td>{{sighting.obsReviewed}}</td>
                    <td>{{sighting.obsValid}}</td>
                </tr>
            </tbody>
        </table>
    </div>
    
  • Test your app to ensure that the recent sightings from eBird are displayed correctly
  • Modify ebird_sightings.html further
    • Add support for filtering
      <div class="row">   
          <div class="form-group col-sm-4 pull-right">
              <input type="text" class="form-control" id="search" ng-model="search" 
                      placeholder="Search Recent Sightings">
          </div>
      </div>
      

      Use search ng-model in ng-repeat directive as shown

          <tr ng-repeat="sighting in recentSightings | filter : search">
      
    • Display up or down icons for boolean columns: Is Private?, Reviewed? and Valid?
      <td>
          <button type="button" class="btn"  
              ng-class="{'btn-success' : sighting.locationPrivate, 'btn-danger' : !sighting.locationPrivate}">
              <span ng-show="{{sighting.locationPrivate}}" class="glyphicon glyphicon-thumbs-up"></span>
              <span ng-show="{{!sighting.locationPrivate}}" class="glyphicon glyphicon-thumbs-down"></span>
          </button>
      </td>       
      

      Notes:

      • btn-success or btn-danger class is added based on value of the locationPrivate field using ng-class directive.
      • glyphicon glyphicon-thumbs-up or glyphicon glyphicon-thumbs-down is displayed based on value of the locationPrivate field in the data.

      Make similar changes for Reviewed? and Valid? columns

      <td>
          <button type="button" class="btn" 
              ng-class="{'btn-success' : sighting.obsReviewed, 'btn-danger' : !sighting.obsReviewed}">
                  <span ng-show="{{sighting.obsReviewed}}" class="glyphicon glyphicon-thumbs-up"></span>
                  <span ng-show="{{!sighting.obsReviewed}}" class="glyphicon glyphicon-thumbs-down"></span>
          </button>
      </td>
      <td>
          <button type="button" class="btn" 
              ng-class="{'btn-success' : sighting.obsValid, 'btn-danger' : !sighting.obsValid}">
                  <span ng-show="{{sighting.obsValid}}" class="glyphicon glyphicon-thumbs-up"></span>
                  <span ng-show="{{!sighting.obsValid}}" class="glyphicon glyphicon-thumbs-down"></span>
          </button>
      </td>
      
  • Run application by executing mvn clean spring-boot:run on the command prompt. Test your changes by pointing your browser to http://localhost:8080 Recent Sightings from ebird.org

  • Now that the basic integration with ebird.org is working fine, let’s integrate HTML5 Geolocation APIs and Google Geocoding APIs.

    • The HTML5 geolocation API lets a user share his/her current location with the trusted web sites. Most modern browsers on desktop and mobile devices support geolocation APIs. Because of the privacy concerns, sharing user’s location is always opt-in. The user may choose not to share his/her location with your application.

      Since geolocation API is asynchronous in nature and may take a while to get the data, I want to initiate the location detection as soon as my application is loaded. AngularJS provides a run block that is executed as soon as the main module starterApp defined in main.js is loaded. This is almost like a main method in Java application.

      Note: As soon as geolocation data is available, model in $rootScope is updated. Data binding feature in AngularJS ensures that the view or UI is updated correctly.

      • Modify main.js to include a run block
        starterApp.run(function($rootScope) {
            $rootScope.geoLocation = {status: "LOCATING"}
            navigator.geolocation.getCurrentPosition(function(location) {
                //Success callback
                $rootScope.geoLocation.coords = location.coords;
                $rootScope.geoLocation.status = "AVAILABLE";
                console.log(angular.toJson($rootScope.geoLocation));
            }, function(positionError) {
                //ERROR callback
                console.log("ERROR : " + angular.toJson(positionError));
                $rootScope.$apply(function() {
                    $rootScope.geoLocation.status = "UNAVAILABLE";
                }); 
            }); 
        });                   
        

        Notes:

        • The geolocation API centers around a new property on the global navigator object: navigator.geolocation
        • I have used the simplest way to get geolocation from the browser
          navigator.geolocation.getCurrentPosition(function() {
              ...
          });
          

          This implementation doesn’t detect whether the user’s browser supports geolocation APIs or not. I’ll, later integrate with the Google geolocation APIs to get location using user-specifed zip or postal code.

        • If user’s current location is successfully obtained, success callback is invoked otherwise error callback is invoked. The success callback function stores the location.coords in a $rootScope variable $rootScope.geoLocation.coords so that is is available to all other scopes created in the application.
    • Once user’s current location (latitude and longitude) is obtained, use Google Reverse Geocoding Api to convert geographic coordinates into a human-readable address.

      • Begin by creating <project folder>/src/main/resources/static/app/js/controllers\googleApisCtrl.js file to implement factories and controllers needed to interact with the Google APIs
        angular.module("starterApp.googleApis", ["ngResource"])
            .factory('googleGeocodingApi', function($resource) {
                return $resource('http://maps.googleapis.com/maps/api/geocode/json', 
                                 {sensor: true});
            })
            .factory("googleGeocodingApiService", ['googleGeocodingApi', function(googleGeocodingApi) { 
                return {
                    latlngLookup: function(address, dataHandler) {              
                        googleGeocodingApi.get({'address': address}, function(data){
                            var geocodingData = {address:{}, coords: {}};
                            console.log(data);
                            geocodingData.address.status = data.status;
                            if (data.status == "OK") {
                                geocodingData.address.formatted = data.results[0].formatted_address;
                                geocodingData.coords.latitude = data.results[0].geometry.location.lat; 
                                geocodingData.coords.longitude = data.results[0].geometry.location.lng;
        
                                data.results[0].address_components.forEach(function(item) {
                                    if (item.types.indexOf("postal_code") > -1) {
                                        geocodingData.address.postalCode = item.long_name;
                                    } else if (item.types.indexOf("administrative_area_level_2") > -1) {
                                        geocodingData.address.county = item.long_name;
                                    } else if (item.types.indexOf("administrative_area_level_1") > -1) {
                                        geocodingData.address.state = item.long_name;
                                    } else if (item.types.indexOf("country") > -1) {
                                        geocodingData.address.country = item.long_name;
                                    }
                                });
                            }
        
                            if (dataHandler) {
                                dataHandler(geocodingData);
                            }
                        });
                    },
                    addressLookup : function(lat, lng, dataHandler) {
        
                        googleGeocodingApi.get({'latlng': lat + "," + lng}, function(data){
                            var geocodingData = {address:{}, coords: {}};
        
                            console.log(data);
                            geocodingData.address.status = data.status;
                            if (data.status == "OK") {
                                var result = data.results[0];
                                geocodingData.address.formatted = result.formatted_address;
                                result.address_components.forEach(function(item) {
                                    if (item.types.indexOf("postal_code") > -1) {
                                        geocodingData.address.postalCode = item.long_name;
                                    } else if (item.types.indexOf("route") > -1) {
                                        geocodingData.address.route = item.long_name;
                                    } else if (item.types.indexOf("locality") > -1) {
                                        geocodingData.address.locality = item.long_name;
                                    } else if (item.types.indexOf("administrative_area_level_2") > -1) {
                                        geocodingData.address.county = item.long_name;
                                    } else if (item.types.indexOf("administrative_area_level_1") > -1) {
                                        geocodingData.address.state = item.long_name;
                                    } else if (item.types.indexOf("country") > -1) {
                                        geocodingData.address.country = item.long_name;
                                    }
                                });
                            }
        
                            if (dataHandler) {
                                dataHandler(geocodingData);
                            }
                        });
                    }
                }
            }]);
        

        Notes

        • Factory googleGeocodingApi returns $resource that uses the default Google Geocoding API Format.
          • /json in the URL indicates the the results will be returned in JSON format.
          • {sensor: true} indicates that the geocoding request comes from a device with a location sensor. This value can be changed during actual query or get call.
        • googleGeocodingApiService uses the googleGeocodingApi factory to perform Google Geocoding Lookups. It defines 2 methods
          • latlngLookup(address, dataHandler) performs a reverse lookup to convert user specified zip code or postal code into geographic coordinates like latitude and longitude.
            dataHandler is a callback function defined in the application that handles the location data retrieved using Google API.
          • addressLookup(lat, lng, dataHandler) performs a reverse lookup to convert geographic coordinates like latitude and longitude into human-readable address.
            dataHandler is a callback function defined in the application that handles the location data retrieved using Google API.
      • Modify run block defined in main.js to use googleGeocodingApiService to perform addressLookup.
        starterApp.run(function($rootScope, googleGeocodingApiService) {
            $rootScope.geoLocation = {status: "LOCATING"}
            navigator.geolocation.getCurrentPosition(function(location) {
                //Success callback - Use Google Geolocation API to get the address
                $rootScope.geoLocation.coords = location.coords;
                $rootScope.geoLocation.status = "AVAILABLE";
                googleGeocodingApiService.addressLookup(location.coords.latitude, 
                                                        location.coords.longitude, function(data) {
                    $rootScope.geoLocation.address = data.address;
                    $rootScope.zipcode = data.address.postalCode;
                });
                console.log(angular.toJson($rootScope.geoLocation));
            }, function(positionError) {
                //ERROR callback
                console.log("ERROR : " + angular.toJson(positionError));
                $rootScope.$apply(function() {
                    $rootScope.geoLocation.status = "UNAVAILABLE";
                }); 
            }); 
        });
        
        • Note that googleGeocodingApiService is injected into the run block by AngularJS.
        • $rootScope object geoLocation is available to all the child scopes.
      • Modify ebird_sightings.html file to use the user location data. Add a form that displays the zip, address, latitude and longitude information retrieved using HTML5 Geolocation and Google Geocoding APIs.
        Bird Log Current Location
        Add the following code to ebird_sightings.html.

        <div class="row">   
            <div class="panel panel-default">
                <div class="panel-body">
                    <div class="alert alert-success" ng-show="geoLocation.status=='AVAILABLE'">
                        Thank you for sharing your location, your current location details have been pre-populated.
                    </div>
                    <div class="alert alert-info" ng-show="geoLocation.status=='LOCATING'">
                        Trying to find your current location, please be patient.
                    </div>
                    <div class="alert alert-danger" ng-show="geoLocation.status=='UNAVAILABLE'">
                        Unable to find your current location. Try using zip code lookup.
                    </div>
                    <form role="form">
                      <div class="form-group col-sm-2">
                        <label for="zip">Zipcode</label>
                        <div class="input-group">
                            <input type="text" class="form-control" id="zip" placeholder="Enter Zipcode" 
                                    ng-model="zipcode">
                            <span class="input-group-btn">
                                <button class="btn btn-default" type="button" ng-click="findLocationUsingZip()">
                                    <span class="glyphicon glyphicon-screenshot"></span>&nbsp;
                                </button>
                            </span>
                        </div>
                      </div>
                      <div class="form-group col-sm-3">
                        <label for="location">Location</label>
                        <input type="text" class="form-control" id="location" 
                               ng-model="geoLocation.address.formatted" 
                               tooltip="{{geoLocation.address.formatted}}" disabled>
                      </div>
                      <div class="form-group col-sm-2">
                        <label for="lat">Latitude</label>
                        <input type="number" class="form-control" id="lat" ng-model="geoLocation.coords.latitude" 
                               placeholder="Latitude" disabled>
                      </div>
                      <div class="form-group col-sm-2">
                        <label for="lng">Longitude</label>
                        <input type="number" class="form-control" id="lng" ng-model="geoLocation.coords.longitude" 
                               placeholder="Longitude" disabled>
                      </div>
                      <div class="form-group col-sm-1">
                        <label for="dist">Dist (KM)</label>
                        <input type="number" class="form-control" id="dist" ng-model="params.dist" 
                                placeholder="Enter Radius in KiloMeters">
                      </div>
                      <div class="form-group col-sm-1">
                        <label for="lng"># of Days</label>
                        <input type="number" class="form-control" id="lng" ng-model="params.back" 
                                placeholder="Enter # of days">
                      </div>
                      <div class="form-group col-sm-1">
                        <label class="col-sm-1">&nbsp;</label>
                        <button type="button" class="btn btn-default form-control col-sm-1"
                                ng-click="refreshRecentSightings()">
                            <span class="glyphicon glyphicon-refresh"></span>
                        </button>
                      </div>
                    </form>
                </div>  
            </div>  
        </div>
        

        Notes:

        • Alert messages are displayed on the top to let the user know about the status of Geolocation API call. In the following example, success message is shown only if geoLocation.status is set to AVAILABLE. See run block in main.js to see how status is set.
          <div class="alert alert-success" ng-show="geoLocation.status=='AVAILABLE'">
              Thank you for sharing your location, your current location details have been pre-populated.
          </div>
          
        • Human-readable address is displayed in the Location field. This field is readonly. Similarly Geographic coordinates like latitude and longitude retrieved using Geolocation APIs are displayed as read-only.
        • User can change the location using Zipcode field. Clicking lookup button next to Zipcode field calls findLocationUsingZip() function that finds geographic co-ordinates and address using Google APIs.
        • Finally user can click Refresh button next to # of Days field to retrieve current bird sightings near specified location. This invokes refreshRecentSightings() function on the controller that in turn uses eBirdApiFactory to get the latest data.
        • Along with the location, user can also specify distance or radius in KiloMeter and length of history in terms of # of Days to go back. All this information is used to populate param object that is passed to the query call on eBirdApiFactory.
  • Finally modify eBirdApiFactory in ebirdApiCtrl to implement refreshRecentSightings and findLocationUsingZip functions.
    angular.module("starterApp.ebird", ["ngResource"])
        .factory('eBirdApiFactory', function($resource) {
            return $resource('http://ebird.org/ws1.1/data/obs/geo/recent?fmt=json', 
                             {lat: 42.3581, lng: -71.0636, dist: 2, back: 5, maxResults: 500, locale:'en_US'});
        })
        .controller("ebirdApiCtrl", ['$scope', 'eBirdApiFactory', 'googleGeocodingApiService',
                                 function($scope, eBirdApiFactory, googleGeocodingApiService) {
            $scope.recentSightings = [];
            $scope.params = {
                dist: 10, 
                back: 10,
                maxResults: 500,
                //lat: 42.3581,
                //lng: -71.0636,
                hotspot: false
            };
    
            $scope.refreshRecentSightings = function() {
                $scope.params.lat = $scope.geoLocation.coords.latitude;
                $scope.params.lng = $scope.geoLocation.coords.longitude;
                eBirdApiFactory.query($scope.params, function(data){
                    $scope.recentSightings = data;
                });
            };
    
            $scope.findLocationUsingZip = function() {
                console.log($scope.zipcode);
                googleGeocodingApiService.latlngLookup($scope.zipcode, function(data) {
                    $scope.geoLocation.address = data.address;
                    $scope.geoLocation.coords = data.coords;
                    $scope.zipcode = data.address.postalCode;
                });
            }
        }]);
    
  • Modify index.html to include new .js files and add new menu item to see recent bird listings from ebird.org
        <html>
            <head>
                <meta name="viewport" content="width=device-width, initial-scale=1">
                <title>AngularJS Starter App</title>
                <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
    
                <!-- APP: css -->
                <!-- Custom styles for this template -->
                <link rel="stylesheet" href="app/css/main.css">
                <!-- end APP -->
            </head>
            <body ng-app="StarterApp">
                <div class="navbar navbar-default navbar-fixed-top" role="navigation">
                    <div class="container">
                        <div class="navbar-header">
                            <button type="button" class="navbar-toggle" data-toggle="collapse"
                                data-target=".navbar-collapse">
                                <span class="sr-only">Toggle navigation</span> 
                                <span class="icon-bar"></span> 
                                <span class="icon-bar"></span> 
                                <span class="icon-bar"></span>
                            </button>
                            <a class="navbar-brand" href="/">My Bird Log</a>
                        </div>
                        <div class="collapse navbar-collapse">
                            <ul class="nav navbar-nav">
                                <li class="active"><a href="/">Home</a></li>
                                <li><a href="/ebird">eBird Sightings</a></li>
                                <li><a href="about">About</a></li>
                            </ul>
                        </div> <!--/.nav-collapse -->
                    </div>
                </div>
    
                <!-- Add your site or application content here -->
                <div class="container"> 
                    <div ng-view></div>
                </div>      
    
                <div id="footer">
                    <div class="container">
                        <p class="text-muted">Starter App : AngularJS, Spring Boot and Maven</p>
                    </div>
                </div>
    
                <!-- LIBS: js -->
                <script src="bower_components/jquery/dist/jquery.js"></script>
                <script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
                <script src="bower_components/angular/angular.js"></script>
                <script src="bower_components/angular-route/angular-route.js"></script>
                <script src="bower_components/angular-resource/angular-resource.js"></script>
                <!-- end LIBS -->
    
                <!-- APP: js -->
                <script src="app/js/main.js"></script>
                <script src="app/js/controllers/homeCtrl.js"></script>
                <script src="app/js/controllers/ebirdCtrl.js"></script>
                <script src="app/js/controllers/aboutCtrl.js"></script>
                <script src="app/js/controllers/googleApisCtrl.js"></script>
                <!-- end APP -->
    
            </body>
        </html>
    
  • Lastly, handle /ebird route in main.js
    starterApp.config(function($routeProvider, $locationProvider) {
        $locationProvider.html5Mode(true);
        $routeProvider.when("/", {
            templateUrl: "app/views/home.html",
            controller: "HomeCtrl"
        })
        $routeProvider.when("/ebird", {
            templateUrl: "app/views/ebird_sightings.html",
            controller: "ebirdApiCtrl"
        })
        .when("/about", {
            templateUrl: "app/views/about.html",
            controller: "AboutCtrl"
        })
        .otherwise({redirectTo : "/"});
    });
    

Sum it up

There you go, I have my Bird Log ready now. It demonstrates how easy it is to build a web application using Spring Boot, AngularJS and Maven.
Bird Log - Home
Bird Log - ebird sightings

Discussion

No comments yet.

Post a Comment


*

Categories