Introduction
Location-based services (LBS) are becoming more and more popular nowadays. There are several mobile applications that detect user's current geographical location, provide related information about the location and also display location in Web. This article discusses how to develop a similar application (both mobile and web part) that will allow the user to save their location from mobile and display their current location in web pages with location history browsing in map. I will show a live demonstration where all these are working together!
Background
Last year, I started to develop my personal website, one thing piqued my interest. I was thinking about how I can make my homepage different from others. I was fascinated by Google Maps for mobile and thought why not I develop a similar thing - a small application that will show my current location in my homepage. I started digging into this topic and it took me no time to find JSR 179 - J2ME Location-Based Services. I could use any of the available applications to show my location (e.g., Google Latitude), but I was interested to play with my GPS traces. I also have a mobile device with built-in GPS module (a common feature for modern Smartphone), so the platform is set for development :).
Technologies Used
Although I will be using J2ME Wireless Toolkit 2.5.2, PHP, HTML, CSS, jQuery, MySQL (a little bit of everything :P) in this article, strong technical background on these is not required. You can use any combination of technologies to develop a similar application.
A Quick Tour
Let's have a quick view on what we will be developing throughout this article. Following are the screenshots of the Mobile application:
And here is the Web application:
URLs for the web application: My Current Location and Browse my GPS traces history.
Ok, enough screenshots. Now we start developing our app. First things first, let's start with the mobile application.
Mobile Application: MapMe
Yes, I call it MapMe. MapMe will perform the following tasks:
- Print current (latitude, longitude) on screen (Figure: 2)
- Display current location in Map (Figure: 3)
- Save current location (Figure: 4)
- Continuously save current location, means if you turn this mode on, the application will keep saving your location as you move until you stop this mode. (Figure: 5)
Don't panic if your mobile does not have GPS unit. I will discuss how to detect location with a non GPS phone later in this article.
Ok, let's start coding! I am using NetBeans IDE. Let's create a J2ME project in NetBeans and add a MIDlet
class, call it MapMe
. Let's add five Command
s ("Locate Me" ,"Map Me", "Save Me", "About Me", "Start") in it to perform the operations. (I will not discuss J2ME related details here, i.e. what is Command
, MIDlet
, etc.). In the getmainForm()
method, we initialize LocationProvider
based on specified Criteria
.
Criteria cr = new Criteria();
cr.setHorizontalAccuracy(Criteria.NO_REQUIREMENT);
cr.setVerticalAccuracy(Criteria.NO_REQUIREMENT);
cr.setCostAllowed(true); cr.setPreferredPowerConsumption(Criteria.POWER_USAGE_HIGH);
this.locationProvider = LocationProvider.getInstance(cr);
Now we start writing getLocation()
method in Helper
class. This method will read current latitude, longitude, altitude and return them. Here is the code:
public static double[] getLocation(LocationProvider lp) throws Exception
{
double []arr = new double[3];
if(lp != null)
{
switch(lp.getState())
{
case LocationProvider.AVAILABLE:
Location l = lp.getLocation(-1);
QualifiedCoordinates c = l.getQualifiedCoordinates();
if(c != null )
{
double lat = c.getLatitude();
double lon = c.getLongitude();
double alt = c.getAltitude();
String latStr = Double.toString(lat).substring(0,7);
String lonStr = Double.toString(lon).substring(0,7);
lat = Double.parseDouble(latStr);
lon = Double.parseDouble(lonStr);
arr[0] = lat;
arr[1] = lon;
arr[2] = alt;
}
else
{
throw new Exception("Co ordinate is null!!");
}
break;
default:
break;
}
}
return arr;
}
Now when user hits "Locate Me" menu, we just need to call getLocation()
method and print the array on display. It's as simple as that. Now, we want to display the location in a map. I am using MidMaps: Google Maps Java ME library. When user hits "Map me" menu, we will set MapViewer
class as our current display and pass location data to it so that it can render the map.
public class MapViewer extends Canvas implements GoogleStaticMapHandler,CommandListener
{
private String apiKey = "Your-GoogleMap(API-v2)-apiKey" ;
public MapViewer(MIDlet midlet, Displayable mainScreen, double lat, double lon)
{
this.lat = lat;
this.lon = lon;
this.mainScreen = mainScreen;
this.midlet = midlet;
this.addCommand(this.backCommand =
new Command("Back", "Back to Search", Command.BACK, 0));
this.addCommand(this.zoomInCommand = new Command("Zoom in", Command.OK, 1));
this.addCommand(this.zoomOutCommand = new Command("Zoom out", Command.OK, 2));
this.setCommandListener(this);
this.gMaps = new GoogleMaps(this.apiKey);
map = gMaps.createMap(getWidth(), getHeight(), GoogleStaticMap.FORMAT_PNG);
map.setHandler(this);
map.setCenter(new GoogleMapsCoordinates(this.lat, this.lon));
GoogleMapsMarker redMarker =
new GoogleMapsMarker(new GoogleMapsCoordinates(this.lat, this.lon));
redMarker.setColor(GoogleStaticMap.COLOR_RED);
redMarker.setSize(GoogleMapsMarker.SIZE_MID);
map.addMarker(redMarker);
map.setZoom(15);
map.update();
}
protected void paint(Graphics g)
{
map.draw(g, 0, 0, Graphics.TOP | Graphics.LEFT);
}
}
And in the command handler of "Map Me" menu in MapMe
class, we write the following code to display current location in Map:
public void commandAction(Command command, Displayable displayable)
{
if(command == this.getcmdMapMe())
{
try
{
double []arr = Helper.getLocation(this.locationProvider);
this.getDisplay().setCurrent
(new MapViewer(this,this.mainForm,arr[0],arr[1]));
}
catch(Exception ex)
{
this.strLat.setText(ex.getMessage());
}
}
}
At this point, we have the necessary methods to detect location and display them. Job half done! Next is just to save them in database. Let's finish those asap. First let's create a table in database (MySQL):
CREATE TABLE IF NOT EXISTS `current_location` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`lat` double NOT NULL,
`lon` double NOT NULL,
`recorded_datetime` datetime NOT NULL,
PRIMARY KEY (`id`)
);
Here is our method to save the current location:
public static boolean saveLocation(String baseUrl, double lat, double lon)
{
InputStream is =null;
StringBuffer sb = null;
HttpConnection http = null;
boolean result = false;
try
{
baseUrl +="&lat=" + Double.toString(lat) +
"&lon=" + Double.toString(lon) + "&time=" + Helper.getNowInDate();
baseUrl = Helper.EncodeURL(baseUrl);
http = (HttpConnection) Connector.open(baseUrl);
http.setRequestMethod(HttpConnection.GET);
if (http.getResponseCode() == HttpConnection.HTTP_OK)
{
sb = new StringBuffer();
int ch;
is = http.openInputStream();
while ((ch = is.read()) != -1)
{
sb.append((char) ch);
}
result = true;
}
else
{
result = false;
}
}
catch (Exception ex)
{
result = false;
}
finally
{
}
}
We also need to write a few lines of server side code to handle this request and save data in the database. I will be using PHP for this purpose. Here it is...
Now, we have almost completed one full cycle. That is detecting and saving location in server. However, we are still missing one thing. In the above approach, user needs to click every time in menu to save location, what if we could write a service that will continuously save location (e.g. save location after every five minutes)? That's also possible and in fact very easy to implement with Location
API. We need to implement LocationListener
interface and thus code two methods locationUpdated()
and providerStateChanged
. We will add two commands "Start" and "End" to fulfill this purpose. Whenever location is updated, locationUpdated()
method is called where we save the latest location in database. So, think you are traveling and turn this service on, the whole path is saved in database and then in web application you can create cool animations that will traverse the path according to the increasing/decreasing order of time! Well, we are not so far from that. Let's now finish implementing this interface:
public void locationUpdated(LocationProvider provider, Location location)
{
if (location != null && location.isValid())
{
QualifiedCoordinates qc = location.getQualifiedCoordinates();
boolean isSaved = Helper.saveLocation(URL, qc.getLatitude(), qc.getLongitude());
String text = isSaved == true ? "Location Saved " + ++count : "Failed";
strLat.setText(text);
}
}
public void providerStateChanged(LocationProvider lp, int i)
{
}
Each time a location record is saved, we update a count
variable and print it. So, while running this service, we can have a hint about how many times it has stored record. Now all we need is to register our MapMe MIDlet to LocationListener
. So we write:
locationProvider.setLocationListener(MapMe.this, 10, -1, -1);
locationProvider.setLocationListener(null, -1, -1, -1);
Yes, it's as simple as that. But there is still one issue, our MapMe
class implements CommandListener
as well and thus handles Command
(i.e. "Map Me" click events). So, when we hit "Start" menu, commandaction
event thread handles this and as long as this thread is not finished, no other button events can not be fired. But as soon as user hits "Start", a pop up dialog box will appear asking for permission to use GPS data. But user will not be able to click it because the commandaction
is not finished yet. A deadlock!! Yes, it is so. So, what we actually have to do is to start and stop listener in different threads:
new Thread()
{
public void run()
{
locationProvider.setLocationListener
(MapMe.this, 10, -1, -1);
}
}.start();
new Thread()
{
public void run()
{
locationProvider.setLocationListener(null, -1, -1, -1);
}
}.start();
That's all! We are at the end of developing a mobile application that can save our location in database (not only once, but continuously in a repeated interval and also shows the position in Map).
Web Application
We now have lots of GPS traces in database. Let's start to play with those! I have developed two simple applications with these:
- Displaying my current location on my homepage and implement a timer so that every time I update my location, user can get real time feedback (Figure 6)
- Location history browsing with navigation tools, i.e., play, pause, etc. (Figure 7)
Auto Updating Current Location
If you browse my website, at the right side of my homepage, you will find a map that shows my exact current location. And if I save a location while you are at my homepage, only the map will refresh (as it is implemented in Ajax) and you will experience a sudden move in the map. Isn't that cool? Yes, and it is very easy to implement! All we need is to write some code that will repeatedly look for updated data in the server and as soon as it detects a new record, i.e., if current latitude and longitude do not matches with the previous one, a change has occurred and it will update the map. Let's first write getCurrentLocation()
method at the server side:
function getCurrentLocation()
{
$strQuery = "select lat, lon, date_format
(recorded_datetime, '%M %d %Y %h:%i %p') as recorded_time from
current_location order by id desc limit 1";
$resId = executeQuery($strQuery);
if($resId)
{
$data = getRecordsArray($resId);
return json_encode(array('lat'=>$data["lat"],
'lon'=>$data["lon"],'time'=>$data["recorded_time"]));
}
}
I have picked the top row (and also the most recent row) from current location table and returned it encoding in json. In the web site, a JavaScript timer will handle it to load the map and keep querying it in a regular interval to detect change. I will write these few lines of JavaScript using jQuery library. Here it is:
<script type="text/javascript">
$(function(){
var jsonUrl = "Functions/getLatLon.php";
var myLocation,map,marker,lat,lon;
lat = lon = -1;
$.ajaxSetup({cache: false});
$('#mapDiv').oneTime("1s",'timerOneTime',function(i)
{
$.getJSON(jsonUrl,{ action: "getCurrentLocation"}, function(data)
{
lat = data.lat;
lon = data.lon;
var text = data.lat + ' ,' + data.lon;
var latlng = new google.maps.LatLng(lat, lon);
var options = {
zoom: 15,
center: latlng,
mapTypeId: google.maps.MapTypeId.ROADMAP,
mapTypeControl: false,
zoomControl: true,
zoomControlOptions: {
style: google.maps.ZoomControlStyle.SMALL
},
streetViewControl: false
};
map = new google.maps.Map(document.getElementById('mapDiv'), options);
var marker = new google.maps.Marker({
position: latlng
});
marker.setMap(map);
}
);
});
$('#mapDiv').everyTime("5s",'timerEveryTime',function(i)
{
$.getJSON(jsonUrl,{ action: "getCurrentLocation"}, function(data)
{
if((lat != data.lat || lon != data.lon) && map != null)
{
lat = data.lat;
lon = data.lon;
var latlng = new google.maps.LatLng(lat, lon);
if (marker != null)
{
maker.setMap(null);
}
map.setCenter(latlng);
var marker = new google.maps.Marker({
position: latlng
});
marker.setMap(map);
}
}
);
});
});
</script>
Location History Browser
Here mapDiv
is the div
where I am loading the map. So, now if you set this up in your homepage, whenever someone comes to that page, he/she will see your exact location and also get notification of Location
change. Ok, we are almost at the end of this article. One more interesting thing I will show is the Location History Browser. Our current_location
table consists of all the GPS traces saved in order of time. So, we can easily get the traces for any specific date! The idea is to allow a user to see the traces of any specific date and develop some navigation tools to play with them. First, let's finish a method getLocationByDate($date)
which will return json payload of GPS traces for a specific date:
function getLocationByDate($date)
{
$str = "select lat, lon, date_format(recorded_datetime, " +
"'%M %d %Y %h:%i %p') as recorded_date,date_format(recorded_datetime," +
" '%h:%i:%s %p') as recorded_time from current_location " +
"where STR_TO_DATE(recorded_datetime,'%Y-%m-%d')= '".$date."' order by id";
$resId = executeQuery($str);
if($resId)
{
$arr = array();
$i = 0;
while($data = getRecordsArray($resId))
{
$arr[$i] = array('lat'=>$data["lat"], 'lon'=>$data["lon"],
'date'=>$data["recorded_date"], 'time'=>$data["recorded_time"]);
$i++;
}
return json_encode($arr);
}
}
Now we can just call getLocationByDate($date)
with a date and populate the data in map. So, the history browser is done! Wait; there are still some things we are missing. Say I have 50 GPS traces saved for one day and does that actually make any sense to display 50 points in the map? Will it add any value to the user? For a user, these are just random points and how he/she will get idea where I visited on that day? Say, I was at place 'A' at 7 am and at place 'B' at 9 am. These will appear as two points, but no time is attached. Here is what I am going to do to make these really usable to the user:
- Every point in the map will be clickable, means if you click a point in the map, the map will pan to that position, i.e., treating that point as a center point and a small box just below the point will show the time! (Remember, the
recorded_datetime
field in current_location
table) - We will add 5 navigation buttons (actually we are going to develop a GPS trace player!). They are:
- First trace: Move the map to the first recorded trace of that day.
- Previous trace: Move the map to the previous trace from currently selected trace thus enabling user to traverse the map in decreasing order of time.
- Play/Pause: Yes, Play! Play the GPS traces. This will start playing the traces from currently selected trace. Means, the map will move automatically to the next point in order of time and keep moving until user hits Pause. To make the transition smooth, I have added some delay before every move. Trust me, you will like the animation.
- Next trace: Move the map to the next trace from currently selected trace thus enabling user to traverse the map in increasing order of time.
- Last trace: Move the map to the last recorded trace of that day.
Now we know what to do! Let's finish the code. jQuery toolbar buttons exactly meets my user interface requirement. So, here is the code:
<script type="text/javascript">
$(function(){
var date = new Date();
var currentMonth = date.getMonth();
var currentDate = date.getDate();
var currentYear = date.getFullYear();
var baseUrl = "Functions/getLatLon.php";
var dateFormat = 'yy-mm-dd';
var map, marker, timerId;
var markers = [];
var records = [];
var index = -1;
map = new GMap2(document.getElementById('mapDiv'));
map.addControl(new GMapTypeControl());
map.addControl(new GLargeMapControl3D());
var center = new GLatLng(23.7804, 90.404);
map.setCenter(center, 10);
$.ajaxSetup({cache: false});
$( "#datepicker" ).datepicker({
maxDate : new Date(currentYear, currentMonth, currentDate),
onSelect: function(dateText, inst){
$("#message").hide();
if (timerId != null){
clearInterval(timerId);
}
if ($( "#play" ).text() == "Pause"){
$("#play").click();
}
var jsonUrl = baseUrl + "?action=getLocationByDate&date=" + dateText;
$.getJSON(jsonUrl, function(json){
map.clearOverlays();
timerId = null;
markers = [];
records = [];
index = -1;
$.each(json, function(key, value){
var point = new GLatLng(value.lat, value.lon);
marker = new GMarker(point);
map.addOverlay(marker);
markers[key] = marker;
records[key] = value;
});
if (markers.length == 0){
$("#dialog-message").text("No Gps Trace is recorded for "+
dateText +
". Please try again.");
$( "#dialog-message" ).dialog('open');
}
else{
$(markers).each(function(i,marker){
GEvent.addListener(marker, "click", function(){
displayPointAndUpdateIndex(marker, records[i], i);
});
});
}
})
}});
$("#dialog-message").dialog({
autoOpen: false,
modal: true,
resizable: false,
buttons: {
Ok: function(){
$(this).dialog("close");
}
}
});
$("#dialog-help").dialog({
autoOpen: false,
width: 500,
modal: true,
buttons: {
Ok: function(){
$(this).dialog("close");
}
}
});
$( "#datepicker" ).datepicker("option", "dateFormat",dateFormat);
$("#message").appendTo(map.getPane(G_MAP_FLOAT_SHADOW_PANE));
$( "#info" ).button({
text:false,
lable: "How to play",
icons: {
primary: "ui-icon-lightbulb"
}
})
.click(function(){
$("#dialog-help").dialog('open');
})
$( "#beginning" ).button({
text: false,
label: "First trace",
icons: {
primary: "ui-icon-seek-start"
}
})
.click(function(){
if (markers.length > 0)
{
index = 0;
displayPoint(markers[index], records[index]);
}
});
$( "#previous" ).button({
text: false,
label: "Previous trace",
icons: {
primary: "ui-icon-triangle-1-w"
}
})
.click(function(){
if (markers.length > 0)
{
if (index <= 0)
{
return;
}
index = index - 1;
displayPoint(markers[index], records[index]);
}});
$( "#play" ).button({
text: false,
icons: {
primary: "ui-icon-play"
}
})
.click(function() {
var options;
if ( $( this ).text() == "Play" ) {
options = {
label: "Pause",
icons: {
primary: "ui-icon-pause"
}
};
$( this ).button( "option", options );
timerId = setInterval(function(){
if(markers.length > 0){
$('#forward').click();
if (index == markers.length - 1){
clearInterval(timerId);
timerId = null;
var option = {
label:"Play",
icons: {
primary: "ui-icon-play"
}
};
$("#play").button( "option", option );
}
}
}, 3000);
} else {
options = {
label: "Play",
icons: {
primary: "ui-icon-play"
}
};
clearInterval(timerId);
timerId = null;
$( this ).button( "option", options );
}
});
$( "#forward" ).button({
text: false,
label: "Next trace",
icons: {
primary: "ui-icon-triangle-1-e"
}
})
.click(function(){
if (markers.length > 0)
{
if (index >= markers.length - 1)
{
return;
}
index++;
displayPoint(markers[index], records[index]);
}});
$( "#end" ).button({
text: false,
label: "Last trace",
icons: {
primary: "ui-icon-seek-end"
}
})
.click(function(){
if (markers.length > 0)
{
index = markers.length - 1;
displayPoint(markers[index], records[index]);
}
});
function displayPointAndUpdateIndex(marker, record, i){
index = i;
displayPoint(marker, record);
}
function displayPoint(marker, record){
map.panTo(marker.getPoint());
var markerOffset = map.fromLatLngToDivPixel(marker.getPoint());
$("#message").show().css({ top:markerOffset.y,
left:markerOffset.x }).html(record.time);
}
});
</script>
You can try it out at http://www.amitdutta.net/gpsTraces.php. Pick May,14,15 or June 10, 2011 as these days have lots of traces. If the points are too dense, zoom the map and then start playing!
Not Yet Implemented
- In the Location History Browser, while user picks a date, the map is filled in the traces of that day. But these traces seem clustered into one place due to the default zoom level. An algorithm should be implemented in the server side to determine the zoom level dynamically based on GPS traces.
- There is no option in "MobileMap" to specify server URL (i.e. where to save data. This is hard-coded and every time I want to change server URL, I need to modify sources, build and re-deploy. I need to implement some way to change default server URL.
Using the Code
There are three simple steps to make the whole system work properly. Here they are:
- Setup Server-Side code and database: For this part, you will need a http server that can run PHP code and a MySQL server to save location traces (I will not discuss how to integrate PHP and MySQL here). For ease of understanding, I assume your HTTP server's URL is "http://www.test.com".
- Go to "ServerSide-setup-for-MobileApp" folder. Run
"current_location.sql"
script to your database (here MySQL) to create tables for the application. Open "config.inc.php"
file and provide the necessary information (username, password, hostname) of your database setup. - Copy
LocationSaver.php
and config.inc.php
to a folder under your http server. Suppose you have copied these files to a folder named "Mobile". So now, "http://www.test.com/Mobile/LocationSaver.php" should be publicly available over the internet, means we can communicate with this from mobile to save GPS traces.
- Setup MobileApp:
- "MapMe" is a Netbeans project. So, it will be easy to open it with Netbeans. It's also possible to compile it with j2ME wireless toolkit's control panel.
- I have used j2me wireless toolkit 2.5.2 because it has
Location
API support. Older version may not have this support. - If Netbeans shows reference problem. Right click on the project and bring up the project properties Window. In the "Optional Packages" list at Right-Bottom, check the checkbox "Location Based API".
- In
"MapMe.java"
line 21, change the "URL" field according to your setup (for example, here the value of "URL" field will be: "http://www.test.com/Mobile/LocationSaver.php?"). "MapViewer.java"
line 20, provide your Google map API key. (for Google map API version-2, get your API key here)
- Setup WebApp:
- Open "Functions\DBInteractor.php" file and provide provide the necessary information (username, password, hostname) of your database setup.
- Now, copy all the files under "Codes\WebApp" to directory where you want to deploy. Say, you have copied these files to "http//www.test/com/WebApp". Now,
"http//www.test/com/WebApp/AutoUpdate.html"
will display your most recent location and "http//www.test/com/WebApp/LocationHistory.html"
will allow user to browse your GPS traces!
Installation
Build the attached "MapMe" project (inside "Codes\MobileApp" directory) and copy "MapMe.jad" and "MapMe.jar" file to your mobile device and install them. For best GPS results, turn on all the GPS positioning methods. In my Nokia N95 8GB, I navigate to "Menu/Tools/Settings/General/Positioning/Positioning methods" and turn on "Assisted GPS", "Integrated GPS" and "Network Based GPS".
For Non GPS Mobile Devices
If your mobile device does not have a built-in GPS, you still can develop a similar application. You need to use CellId (A CellID
is a number which is associated with a specific cell (the radio tower to which your handset is connected)) and use open source database of CellID
s (such as OpenCellID). I would recommend you read this article and this one for non GPS based phones.
What Now?
If you have come here, that means you have gone through this long article (I hope you have not just scrolled down here :P). I would like to hear your thoughts about this implementation. Share your ideas, vote if you have liked it and leave a comment...