Walkers Mapped Gps






4.92/5 (5 votes)
Complete description of an Android gps application with links to Android code.
Introduction
This article is a complete description of an Android gps application.
Links to all the Android code used in this application are provided.
The WalkersMap.zip has the complete project for this application.
I use eclipse Indigo with the Android SDK package. Open the PROJECT FILE
and eclipse should load this application’s code.
Android has a complete description on how to load the SDK, however I found two problems.
There is a problem when using windows 7. We cant load the android package
when eclipse is stored in C:\program. It will load if you run eclipse as administrator.
I stored eclipse in the video folder and then I don’t need right click.
Android virtual devices are communicated with via ports. If you have a zealous firewall
it can block its operation. I needed to add an allowed application to the firewall.
Program operation:
At its basic level the program maps gps locations. We have the following specifications:
1. The map is a 1km grid, it contains no altitude information.
It uses Universal Transverse Mercator geographic coordinates.
2. The user can have a UTM terrain map. Way points from this map can
be entered into the program and displayed along with the gps locations.
3. The map will have a maximum 200km by 200km area.
The display can be zoomed in/out moved left/right, up/down.
The largest display is a 50km by 50 km area. The smallest is 1km by 1km.
4. Gps is a heavy user of battery power so the phone must sleep between gps fixes.
Fixes can be set to automatic. Wake the phone once every 5 minutes for the fix.
5. All the gps locations need to be stored in the phones flash memory.
They are then restored to the application after a phone reboot.
Splash screen:
To do this I use a CountDownTimer.
In onCreate the main.xml has the splash screen image.
We hold this image for 3 seconds then reload the screen with the graphics
map page constructed in mView.
setContentView(R.layout.main); // new CountDownTimer(3000,1000) { public void onFinish() { setContentView(mView); }//end of on finish public void onTick(long arg0){} }.start();
Map Screen:
The screen is constructed in the MapView class. We pass the screen dimensions to the class
and return the screen with all the data points plotted.
DisplayMetrics metrics = new DisplayMetrics(); ////
getWindowManager().getDefaultDisplay().getMetrics(metrics);
screenW =metrics.widthPixels;
screenH = metrics.heightPixels;
mView = new MapView(this,screenW,screenH);
The onDraw method is called every time the screen is redrawn. This happens on startup
and any time I call mView.invalidate(); //redraw screen
public class MapView extends View
{
//
public MapView(Context context, float sW, float sH)
{
super(context);
setFocusable(true);
d = sW/260; //set the screen delta
h = sH; //set screen height
constructSigns(); //construct the button signs
}//end of constructor
@Override protected void onDraw(Canvas canvas)
{
// all graphics done here
drawButtons(canvas);
}//end of onDraw
//
}//end of MapView class
The program uses scaling and shifting to generate the map. We can zoom in/out,
shift left/right, shift up/down. This allows the user to center his locations on the
map at the optimum viewing size.
The drawButtons method makes use of Android Path and matrix API’s.
Integral to constructing the map is the conversion of the lat/long
gps fixes(degrees) into meters.
I use the Universal Transverse Mercator geographic coordinate system to do the conversion.
Universal Transverse Mercator:
The system divides the Earth into sixty zones, each a six-degree band of longitude.
The main reason I use UTM is because most bush walkers will have a UTM map of
the area they traverse. They will plot their walk on the paper map and then select the way points in this application.
They then set continuous mode and the program will plot the gps fixes over the selected way point path.
The conversion mathematics comes from the Defense Mapping Agency Technical Manual.
The conversion is done in the UTS class.
public class UTS
{
static long EastingI = 0; //easting in meters
static long NorthingI = 0; //northing in meters
static int ZoneNumber = 0; //the easting grid zone
static char ZoneDesignator = 'C';
private static double CM = 0;//153;//99; //central meridian
private final static char ZD[] = {'C','D','E','F','G','H','J','K','L','M',
'N','P','Q','R','S','T','U','V','W','X'}; //the zone designators
public static void ConvertGeoUts(double lat, double lon)
{
ZoneNumber = ((int) Math.floor(lon) + 180)/6 + 1;
CM = (ZoneNumber - 1)*6 + 3 - 180;
byte i = 0;
for(i = 0; i < 20; i++)
{
if(lat + 80 < i*8) { break; } //break when find zone
}//end of get zone designator
ZoneDesignator = ZD[i - 1]; // set value
N_GEOtoUTS(lat, lon);
E_GEOtoUTS(lat, lon);
}//end of geo to uts conversion
//
}//end of UTS class
The methods are all static so that they are held permanently in RAM.
This gives faster operation as I do not need to create an instance each time it’s needed.
UTS.ConvertGeoUts(lat , lon); //this is all thats needed
Map Screen Menu:
The menu is defined in the menu.xml file:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@+id/menu_help"
android:icon="@android:drawable/ic_menu_help"
android:title="@string/menu_help" />
<item android:id="@+id/menu_reference"
android:icon="@android:drawable/ic_menu_compass"
android:title="@string/menu_gpsloc" />
<item android:id="@+id/menu_locations"
android:icon="@android:drawable/ic_menu_myplaces"
android:title="View Locations" />
<item android:id="@+id/menu_start"
android:icon="@android:drawable/ic_menu_more"
android:title="Start Continuous" />
<item android:id="@+id/menu_delete"
android:icon="@android:drawable/ic_menu_delete"
android:title="Delete this Walk" />
<item android:id="@+id/menu_cancel"
android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:title="Cancel Continuous" />
</menu>
The menu is implemented by:
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
MenuInflater mi = getMenuInflater();
mi.inflate(R.menu.menu, menu);
return true;
}//end of inflate menu specified in XML menu file
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item)
{
switch(item.getItemId())
{
case R.id.menu_help:
Intent i =new Intent(this,HelpFile.class);
startActivity(i); //show the help file
return true;
case R.id.menu_reference:
Intent igps =new Intent(this,selectReference.class);
startActivity(igps); //show the GPS file
return true;
case R.id.menu_locations:
Intent igl =new Intent(this,ViewLocations.class);
startActivity(igl); //show the GPS file
return true;
case R.id.menu_delete:
DeleteWalk(); //bring up the dialog
return true;
case R.id.menu_start:
long alarm = 300000; //5 minute
if(!continuous){
CG.setContinuous((long) 0, alarm);
continuous = true; //we are running
mView.invalidate(); //redraw screen
}//end of start continuous
return true;
case R.id.menu_cancel:
if(continuous){
CG.cancelContinuous(); //bring up the dialog
continuous = false;
mView.invalidate(); //redraw screen
}//end of cancel continuous
return true;
}//end of case
return super.onMenuItemSelected(featureId, item);
}//end of menu item selected
Map Screen Delete:
If the users wishes to delete the entire walk I ask with a dialog if this is their wish:
private void DeleteWalk() { mDbHelper = new LocationsDatabase(this); mDbHelper.open(); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage("Do you want to delete all gps and way point locations ") .setTitle("'Delete this Walk") .setCancelable(false) .setPositiveButton("Yes Delete All", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { mDbHelper.DeleteAllLocation(); //delete all rows in sqlite NumLoc = 0; // reset the location number Location[0][0]=0; Location[0][1]=0; Location[0][2]=0; Location[0][3]=0; Location[0][4]=0; //reset first gps NumWay = 0; //reset the waypoint number WayPoint[0][0]=0; WayPoint[0][1]=0; mDbHelper.close(); //Don't need now mView.invalidate(); //redraw screen } }) .setNegativeButton("No Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { mDbHelper.close(); //Don't need now dialog.cancel(); } }); final AlertDialog alert = builder.create(); alert.show(); }//end of delete walk
Data Restoration:
Applications are stored in flash memory(sometimes called ROM).
This is non volatile and is not lost when the phone is powered down.
When the application is started, the program is loaded into the RAM to allow direct access by the CPU.
All data that is generated by the program is also stored in RAM.
If the program loses focus because of a phone call or user operation, the data remains in RAM.
If the user or the system puts the phone to sleep, the data remains in RAM.
However if the phone is turned off the RAM loses the program and any data generated.
The program remains in flash memory and is reloaded by the android operating system
when the user selects the program.
Any data you need to restore must be first stored in the flash.
Android provides methods for saving and restoring data in the flash.
I use two types of data storage.
Shared Preferences for individual values, start latitude/longitude and Number of gps/waypoints.
The data is stored in onStop whenever the map page loses focus.
protected void onStop() { super.onStop(); // We need an Editor object to make preference changes. //Store private primitive data in key-value pairs. SharedPreferences settings = getSharedPreferences("gps", 0); //the folder SharedPreferences.Editor editor = settings.edit(); editor.putInt("GpsLocations", NumLoc); editor.putInt("WayPoints", NumWay); editor.putFloat("LatRef", (float) MapView.latRef); editor.putFloat("LonRef", (float) MapView.lonRef); //save these key values // Commit the edits! editor.commit(); }//end of on stop
The data is restored whenever the program is started in onCreate.
SharedPreferences settings = getSharedPreferences("gps",0);
MapView.latRef = settings.getFloat("LatRef", 0);
MapView.lonRef = settings.getFloat("LonRef", 0);
NumLoc = settings.getInt("GpsLocations", 0); //restore gps locations number
NumWay = settings.getInt("WayPoints", 0); //restore way point number
To store the data from 576 gps locations and 50 way points we need a database.
SQLite database:
The database is enabled in the public class LocationsDatabase
In this class we have a nested class that uses the recommended method
to create a new SQLite database.
private static class DatabaseHelper extends SQLiteOpenHelper
{
DatabaseHelper(Context context)
{
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}//end of constructor database is gps
@Override
public void onCreate(SQLiteDatabase db) { db.execSQL(DATABASE_CREATE);}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,int newVersion)
{
db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE);
onCreate(db);
}//end of upgrade database
}//end of nested class DatabaseHelper
The LocationsDatabase class has these methods which are used to store and access
data from the SQLite database(which is in the flash memory).
public LocationsDatabase open() throws SQLException
//
public void close() { mDbHelper.close();} //caller closes data base
//
public long AddLocation(int row, String type, double latoreast, double lonornorth,
double altitude, double time, double accuracy)
//
public Cursor ReadLocation(long rowId) throws SQLException
//
public boolean DeleteAllLocation()
To access the database we need to create an instance of LocationsDatabase, open
the data base and then store the data. Finally we must close the database.
So to store a way point we have:
mDbHelper = new LocationsDatabase(this);
mDbHelper.open();
//
mDbHelper.AddLocation(WalkersMapGps.NumWay + WalkersMapGps.NumLoc,"way",
WalkersMapGps.WayPoint[WalkersMapGps.NumWay][0],
WalkersMapGps.WayPoint[WalkersMapGps.NumWay][1],0,0,0);
WalkersMapGps.NumWay += 1;
//
mDbHelper.close(); //Don't need now
There are two types of data stored in the database. Gps locations labeled “gps” and
way points labeled “way” .To restore all the data we read the data base:
Cursor ReadRow =mDbHelper.ReadLocation(i); //rows 0 1 2 3 ect
startManagingCursor(ReadRow);
String type = ReadRow.getString(ReadRow.getColumnIndexOrThrow
(LocationsDatabase.KEY_TYPE));
if(type.equals("gps"))
{
Location[l][0] = ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_LATOREAST));
Location[l][1] = ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_LONGORNORTH));
Location[l][2] = ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_ALTITUDE));
Location[l][3] = ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_TIME));
Location[l][4] = ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_ACCURACY));
l += 1; //count this one
}//end of its a gps value
else //we have a way point
{
WayPoint[w][0] =(int) ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_LATOREAST));
WayPoint[w][1] =(int) ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_LONGORNORTH));
w += 1; //count this one
}//end of its a way point
Gps Locations:
The GpsActivity class has all the code necessary to get the gps fixes.
If a fix cannot be obtained within 3 minutes the activity will close.
I use a CountDownTime to close the activity.
The top button(gps on/off) can be selected to redirect the user to the Android
location settings page:
gpsOn = (Button) findViewById(R.id.location);
gpsOn.setOnClickListener(new View.OnClickListener()
{
public void onClick(View v) {
Intent i = new Intent(Settings.
ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(i); //show the gps settings
finish(); //exit this program
}//end have click
});////end of click listener
The code to obtain gps fixes is the recommended method:
locationManager = (LocationManager) this.getSystemService(Context.
LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER
, 0, 0, locationListener);
//
// Define a listener that responds to location updates/
LocationListener locationListener = new LocationListener()
{
public void onLocationChanged(Location location)
{
// Called when a new location is found by the network location provider.
makeUseOfNewLocation(location);
}//end of location change
public void onStatusChanged(String provider, int status, Bundle extras) {}
public void onProviderEnabled(String provider) {}
public void onProviderDisabled(String provider) {}
};
private void makeUseOfNewLocation(Location loc)
{
Location mLoc = (Location) loc;
if(GetReference) //this location will be the reference
{
MapView.latRef = mLoc.getLatitude();
MapView.lonRef = mLoc.getLongitude();
GetReference = false; //we have got our reference
}//end of get reference
else //this is a normal location
{
WalkersMapGps.Location[WalkersMapGps.NumLoc][0] = mLoc.getLatitude();
WalkersMapGps.Location[WalkersMapGps.NumLoc][1] = mLoc.getLongitude();
WalkersMapGps.Location[WalkersMapGps.NumLoc][2] = mLoc.getAltitude();
WalkersMapGps.Location[WalkersMapGps.NumLoc][3] = mLoc.getTime();
WalkersMapGps.Location[WalkersMapGps.NumLoc][4] = mLoc.getAccuracy();
if(WalkersMapGps.NumLoc < 574)
{
mDbHelper.AddLocation(WalkersMapGps.NumLoc + WalkersMapGps.NumWay,"gps",
WalkersMapGps.Location[WalkersMapGps.NumLoc][0],
WalkersMapGps.Location[WalkersMapGps.NumLoc][1],
WalkersMapGps.Location[WalkersMapGps.NumLoc][2],
WalkersMapGps.Location[WalkersMapGps.NumLoc][3],
WalkersMapGps.Location[WalkersMapGps.NumLoc][4]); //add location to sqlite
WalkersMapGps.NumLoc += 1;
} //we have one
else{
Toast.makeText(getApplication(), "Only 576 locations allowed", Toast.LENGTH_SHORT).show();
}//end of filled buffer
}//end of get gps location
cancelGps(); //we are finished
}//end of location changed
Set Reference:
The selectReference class has the code to select the start or reference point for the map.
The user can enter a latitude/longitude: They can enter each separately.
Android does not have an edit text box which allows negative float numbers.
To implement this feature I use try catch blocks. To catch a value out of bounds
I throw an exception. The latitude entry is shown:
final EditText ReferenceLat = (EditText) findViewById(R.id.enter_latitude);
ReferenceLat.setText(String.valueOf(MapView.latRef)); //show existing reference
final Button selectLat = (Button) findViewById(R.id.b101);
selectLat.setOnClickListener(new View.OnClickListener()
{
public void onClick(View v) {
try
{
Editable text = ReferenceLat.getText();
double lat = Double.parseDouble(text.toString());
if(lat < -80 || lat > 84) {throw new Exception();}//out of range
MapView.latRef = lat; //enter new reference
}//end of try
catch(Exception e){
Toast.makeText(getApplication(), "Latitude must between -80.0 and 84.0", Toast.LENGTH_SHORT).show();
}//end of catch
}//end have click
});////end of click listener
Continuous Gps:
The gps is a heavy user of battery power.
I require a gps fix once every 5 minutes.
The first gps fix requires the download of the
Ephemeris (precise satellite orbit) for each satellite.
This may take up to 2 minutes.
Subsequent fixes will only take seconds.
So the phone sleeps for most of the time and it wakes to get
a quick gps fix.
This uses very little battery, extending the period the user
can utilize this application.
Firstly in public class ContinuousGps I set the alarm manager.
public ContinuousGps(Context context) {
mContext = context;
mAlarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent i = new Intent(mContext, OnAlarmReceiver.class);
pi = PendingIntent.getBroadcast(mContext, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
}// end of setup alarm manager
public void cancelContinuous(){ mAlarmManager.cancel(pi); }
public void setContinuous(Long taskId, long alarmtime) {
mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 10000,
alarmtime, pi);
}
Then in public class OnAlarmReceiver I need to setup a BroadcastReceiver.
When this is received we wake the phone and hold it on while we call
a service.
public class OnAlarmReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
WakeIntentService.acquireStaticLock(context);
Intent i = new Intent(context, GpsService.class);
context.startService(i);
}//end of on receive method
}//end of alarm receiver class
The service started is in the public class GpsService.
The service starts the GpsActivity which gets the gps fix.
public class GpsService extends WakeIntentService {
public GpsService() {
super("GpsService");
}
@Override
void doGpsWork(Intent intent)
{
Intent i = new Intent(this, GpsActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
}//end of gps work
}//end of gps service
The WakeIntentService class is where we setup the PowerManager.WakeLock.
The code used is the recommended method.
public abstract class WakeIntentService extends IntentService
{
abstract void doGpsWork(Intent intent);
public static final String LOCK_NAME_STATIC="com.carl47.walkers";
private static PowerManager.WakeLock lockStatic=null;
public static void acquireStaticLock(Context context) {
getLock(context).acquire();
}
synchronized private static PowerManager.WakeLock getLock(Context context) {
if (lockStatic==null) {
PowerManager mgr=(PowerManager)context.getSystemService(Context.POWER_SERVICE);
lockStatic=mgr.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
LOCK_NAME_STATIC);
lockStatic.setReferenceCounted(true);
}
return(lockStatic);
}
public WakeIntentService(String name) {
super(name);
}
@Override
final protected void onHandleIntent(Intent intent) {
try {
doGpsWork(intent);
}
finally {
getLock(this).release();
}
}
}//end of wake intent service
Manifest:
All the displayed screens are set to portrait, the map page has
no title bar and uses the full screen.
The manifest contains all the necessary permissions and activities used:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="carl47.com" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="6" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".WalkersMapGps" android:label="@string/app_name" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".GpsActivity" android:label="@string/app_name" android:screenOrientation="portrait"/> <activity android:name=".HelpFile" android:label="@string/app_name" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:screenOrientation="portrait"/> <activity android:name=".GpsSelect" android:label="@string/app_name" android:screenOrientation="portrait" /> <activity android:name=".selectReference" android:label="@string/app_name" android:screenOrientation="portrait"/> <activity android:name=".ViewLocations" android:label="@string/app_name" android:screenOrientation="portrait"/> <activity android:name=".WayPoints" android:label="@string/app_name" android:screenOrientation="portrait"/> <receiver android:name=".OnAlarmReceiver" /> <service android:name=".GpsService" /> </application> </manifest>
Conclusion:
The application works as per the specifications.
It’s published on the Android market. It’s completely free, download it to your Android phone
to see it work as advertised.