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. Useng-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
inng-repeat
directive as shown<tr ng-repeat="sighting in recentSightings | filter : search">
- Display
up
ordown
icons for boolean columns:Is Private?
,Reviewed?
andValid?
<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
orbtn-danger
class is added based on value of thelocationPrivate
field usingng-class
directive.glyphicon glyphicon-thumbs-up
orglyphicon glyphicon-thumbs-down
is displayed based on value of thelocationPrivate
field in the data.
Make similar changes for
Reviewed?
andValid?
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>
- Add support for
- Run application by executing
mvn clean spring-boot:run
on the command prompt. Test your changes by pointing your browser tohttp://localhost:8080
-
Now that the basic integration with ebird.org is working fine, let’s integrate
HTML5 Geolocation APIs
andGoogle 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 arun
block that is executed as soon as the main modulestarterApp
defined inmain.js
is loaded. This is almost like amain
method inJava
application.Note: As soon as
geolocation data
is available, model in$rootScope
is updated.Data binding
feature in AngularJS ensures that theview
orUI
is updated correctly.- Modify
main.js
to include arun
blockstarterApp.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 globalnavigator
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 theGoogle geolocation APIs
to get location using user-specifedzip or postal code
. - If user’s current location is successfully obtained,
success
callback is invoked otherwiseerror
callback is invoked. Thesuccess callback function
stores thelocation.coords
in a $rootScope variable$rootScope.geoLocation.coords
so that is is available to all other scopes created in the application.
- The
- Modify
-
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 implementfactories
andcontrollers
needed to interact with the Google APIsangular.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 actualquery
orget
call.
googleGeocodingApiService
uses thegoogleGeocodingApi
factory to performGoogle Geocoding Lookups
. It defines 2 methodslatlngLookup(address, dataHandler)
performs a reverse lookup to convert user specifiedzip code
orpostal code
intogeographic coordinates
likelatitude
andlongitude
.
dataHandler
is a callback function defined in the application that handles the location data retrieved usingGoogle API
.addressLookup(lat, lng, dataHandler)
performs a reverse lookup to convertgeographic coordinates
likelatitude
andlongitude
into human-readable address.
dataHandler
is a callback function defined in the application that handles the location data retrieved usingGoogle API
.
- Factory
- Modify
run
block defined inmain.js
to usegoogleGeocodingApiService
to performaddressLookup
.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 therun
block by AngularJS. $rootScope
objectgeoLocation
is available to all the child scopes.
- Note that
- Modify
ebird_sightings.html
file to use the user location data. Add a form that displays thezip
,address
,latitude
andlongitude
information retrieved usingHTML5 Geolocation
andGoogle Geocoding
APIs.
Add the following code toebird_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> </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"> </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 ifgeoLocation.status
is set toAVAILABLE
. Seerun
block inmain.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 theLocation
field. This field is readonly. SimilarlyGeographic coordinates
likelatitude
andlongitude
retrieved usingGeolocation APIs
are displayed as read-only.- User can change the location using
Zipcode
field. Clickinglookup
button next toZipcode
field callsfindLocationUsingZip()
function that finds geographic co-ordinates and address usingGoogle APIs
. - Finally user can click
Refresh
button next to# of Days
field to retrieve current bird sightings near specified location. This invokesrefreshRecentSightings()
function on the controller that in turn useseBirdApiFactory
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 populateparam
object that is passed to thequery
call oneBirdApiFactory
.
- Alert messages are displayed on the top to let the user know about the status of
- Begin by creating
- 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
- Finally modify
eBirdApiFactory
inebirdApiCtrl
to implementrefreshRecentSightings
andfindLocationUsingZip
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 inmain.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
.
Discussion
No comments yet.