In this lecture, we learn how to incorporate Google Maps into applications - this is very cool. We have all used Google Maps on laptop browsers and smartphones but only as user up until now.
We will first learn how to install the Android Google Maps v2.0 environment. Then through two simple demos apps we get a sense of the main programming features needed to construct and control maps. I even named a demo app after the city recently described by New York Times journalist Neil MacFarquhar as a "drab industrial city". Cheers Neil - I was born and brought up in the hip, bustling, vibrant town of Coventry. You clearly now "darb" when you see it Neil. I digress. Now we transition from one Android aka Neil to another.
The demo code used in this lecture include:
The Coventry demo is take from here: detect MarkerClick and add Polyline The app detects long click on map and adds a marker. Lines can be drawn between markers using polylines.
These two apps will provide the necessary background to implement maps for [MyRuns4](http://www.cs.dartmouth.e
Note, that when you download the i_am_here.zip and coventrydemo.zip demo apps you will need to replace the Google MAP API key in the Manifest (which is mine) with your own key. These apps will not work if you do not do that. Instructions to do this are given Step 2 below.
Some excellent references.
The detailed process of creating a new Android application that uses the Google Maps Android API v2 requires several steps. In what follows we provide a more truncated set of steps with some screen dumps to help you along
Many of the steps outlined in this section will only have to be performed once, but some of the information will be a handy reference for future applications. The overall process of adding a map to an Android application is as follows:
You have to do step 1 only once for your Eclipse environment. You have to do steps 2-4 for each new project that uses maps. I'll repeat this and paraphrase: for each new project that uses Google Map v2.0 you have to get a new key using the Google APIs Console; then insert that key into your manifest, and finally, you have to add the path to the Google Map library into the new project.
OK. That is the summary of how you do steps 1-4. Now let's provide more details with some illustrative screen dumps.
The API is distributed as part of the Google Play services SDK, which you can download with the Android SDK Manager. To use the Google Maps Android API v2 in your app, you will first need to install the Google Play services SDK. To learn how to install the package, see the Google Play services documentation for full details.
We need to install Google Play to get Google Maps and other parts of the SDK; you need to:
Obtaining a key for your application requires several steps. These steps are outlined here, and described in detail in the following sections.
The Maps API key is based on a short form of your application's digital certificate, known as its SHA-1 fingerprint. Google Maps uses SHA-1 fingerprint in combination with the project name as a way to identify your application.
To display the SHA-1 fingerprint for your certificate, first ensure that you have the certificate itself. To obtain a SHA-1 fingerprint for your certificate read this or if you don't like reading the details follow these instructions:
We use the keytool command to generate a debug signing certificate. To do this open a terminal on your mac (sorry windows people) and issue the following command:
$ keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
You should get the following output with the embedded SHA1: 65:24:35:7B:14:72:FC:B7:35:EF:A9:2E:01:3D:EA:41:E9:67:40:A4
The output from keytool should be:
Alias name: androiddebugkey
Creation date: Aug 19, 2011
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=Android Debug, O=Android, C=US
Issuer: CN=Android Debug, O=Android, C=US
Serial number: 4e4eb341
Valid from: Fri Aug 19 15:02:25 EDT 2011 until: Sun Aug 11 15:02:25 EDT 2041
Certificate fingerprints:
MD5: F3:82:2F:FC:1E:9E:A8:B2:89:48:92:13:AF:B4:CB:F3
SHA1: 65:24:35:7B:14:72:FC:B7:35:EF:A9:2E:01:3D:EA:41:E9:67:40:A4
Signature algorithm name: SHA1withRSA
Version: 3
If it fails, sorry you have to read the information above. One issue could be how to find ~/.android/debug.keystore in Mac OS X for Android? so read that stack overflow posting; summary: you can select Windows > Prefs > Android > Build and you will see a field that tells the location of your debug keystore, as shown below
Note, we replace /Users/atc/.android/debug.keystore with ~/.android/debug.keystore as in the keytool syntax shown in the command line above.
If all fails go back over these steps.
OK. You have your SHA certificate. You need to use this every time you create a new project that uses Google Maps so store your SHA (it does not change) somewhere that you can find later. Or store the command -- you can always re-run it.
as a service for the project.
Now you have to get a project and register for the API. So after you have your signing certificate fingerprint we need to create a project for your application in the Google APIs Console and register for the Maps API.
To that we have to follow these steps:
Click OK, then click "Loading" at the column on the left.
Input your project name, select "Terms and Service" then click "Create" to create a new project.
After creating the project, you can see the project in the project list. Click the project to go to the next step.
You will see a list of APIs. Find Google Maps Android API v2 and click the "Off" button to turn it on.
A dialog will show up, you need to agree the terms of services.
Click Android Key in the following dialog.
In the resulting dialog, enter the SHA-1 fingerprint, then a semicolon, then your application's package name. For example:
You will need the package name from your project -- you find that in the manifest, for example:
package="edu.dartmouth.cs.whereami_6"
You will also need your SHA certificate
65:24:35:7B:14:72:FC:B7:35:EF:A9:2E:01:3D:EA:41:E9:67:40:A4
OK. Now follow the instructions and put your SHA and package name into the field, shown below using a ; between the SHA and package name.
You will also need your SHA certificate
65:24:35:7B:14:72:FC:B7:35:EF:A9:2E:01:3D:EA:41:E9:67:40:A4;edu.dartmouth.cs.whereami_6
Now you will see the new key, as shown below
The API key is:
AIzaSyBzpZBg7esbJeF5UXYHnPX0ljwQfRSbNW4
You need to add that key to the mainfest of the project with the package name edu.dartmouth.cs.whereami_6. This key will only work with this project and no other one.
One problem I had -- was I clicked not on New Android Key but by accident (because I'm an idiot) I clicked on Create New Browser Key. Guess what? My code would not work but compiled OK. I saw from the CatLog that the map was not loading from Google and knew the key was screwed up but it took me a couple of hours to see the nose on my face -- which is sizeable as you well know.
There are a number of additions to the Manifest file (we discuss them in this write up) but we focus here on the meta-data. Once you have the API key you need to add it into the Manifest as part of the meta-data, as shown below.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
[Snip code]
........
........
<permission
android:name="edu.dartmouth.cs.whereami_5.MAPS_RECEIVE"
android:protectionLevel="signature" />
<uses-permission android:name="edu.dartmouth.cs.whereami_5.MAPS_RECEIVE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<application
[Snip code]
........
........
<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="AIzaSyBzpZBg7esbJeF5UXYHnPX0ljwQfRSbNW4" />
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
</application>
</manifest>
There are the a number of additions to the manifest needed -- not necessarily in the sequential order, as they appear in the manifest:
meta-data: The first meta-data element sets the key com.google.android.maps.v2.API_KEY to the value of your key -- that is, AIzaSyBzpZBg7esbJeF5UXYHnPX0ljwQfRSbNW4 -- and makes the API key visible to any MapFragment in your application. The second meta-data element sets the key com.google.android.gms.version to the value of your Google Play Services version. This is required for any app that uses Google Play Services.
permission: We set the permission for the app to receive maps. Make sure you add this permission of maps will not load.
uses-permission: The application must be granted uses permission in order for it to operate correctly. Permissions are granted by the user when the application is installed, not while it's running. There are a number related to maps:
Almost there. You need to set up your path library for Google Map APIs:
See below for some image dumps of the process:
First, goto Project > Properties and select Android (in the left panel) as shown below.
Under Library select Add and select google-play-services_lib and click OK as shown below.
In your project you will see that the library has been installed under a number of libraries folders, as shown below.
OK you are all set in terms of adding the library to your project. Again, you have to repeat this step for each project that uses Google Maps.
The first application we look at this is an extension of the applications we developed for the lecture on the LocationManger. As shown in the image below the app lists:
The cool thing about this app is that as you move around it will update your position on the map. It tracks you. There is little control over the app. You can move the map around or use the simple zoon in / zoom out buttons on the map -- that is about it.
Let's discuss the code. Note, in the code examples below we snip some of the code that we have already discussed in the pervious lecture. You can look at the demo app source code to see the complete source code.
Much of the structure of the code is familiar now.
The code first gets a reference to a GoogleMap using getFragmentManager() on MapFragment set up in layout/activity_main.xml, as shown below in the layout file
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:id="@+id/locinfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<fragment
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.MapFragment"/>
</LinearLayout>
The getMap() method renders the Google Map returned from the server into the MapFragment in layout. The type of map is then set to normal.
There are a number of types of maps that can be selected:
Change the type of the map in your code and look at the map rendered.
mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
After the map type is set we get the current location and set a marker at that location and zooms in. The location manager sets up the time and distance parameters as well as the call back listener for location updates:
locationManager.requestLocationUpdates(provider, 2000, 10,
locationListener);
We discussed these call backs in the last lecture. So check that out again if you need to. The helper function then gets called to update the map if necessary.
public class WhereAmI extends Activity {
public GoogleMap mMap;
public Marker whereAmI;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Get a reference to the MapView
mMap = ((MapFragment) getFragmentManager().findFragmentById(R.id.map))
.getMap();
// Configure the map display options
mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
LocationManager locationManager;
String svcName= Context.LOCATION_SERVICE;
locationManager = (LocationManager)getSystemService(svcName);
[Snip code]
........
........
........
Location l = locationManager.getLastKnownLocation(provider);
LatLng latlng=fromLocationToLatLng(l);
whereAmI=mMap.addMarker(new MarkerOptions().position(latlng).icon(BitmapDescriptorFactory.defaultMarker(
BitmapDescriptorFactory.HUE_GREEN)));
// Zoom in
mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latlng,
17));
updateWithNewLocation(l);
locationManager.requestLocationUpdates(provider, 2000, 10,
locationListener);
}
public static LatLng fromLocationToLatLng(Location location){
return new LatLng(location.getLatitude(), location.getLongitude());
}
Each time the callback onLocationChanged() is called the map is updated simply by calling the helper function discussed below. There is no action for the other callbacks in this code -- there really should be.
private final LocationListener locationListener = new LocationListener() {
public void onLocationChanged (Location location) {
updateWithNewLocation(location);
}
public void onProviderDisabled(String provider) {}
public void onProviderEnabled(String provider) {}
public void onStatusChanged(String provider, int status,
Bundle extras) {}
};
The helper function simply has the current location passed to it. It first removed the current marker and redraws a new marker at the new location.
private void updateWithNewLocation(Location location) {
TextView myLocationText;
myLocationText = (TextView)findViewById(R.id.myLocationText);
String latLongString = "No location found";
String addressString = "No address found";
if (location != null) {
// Update the map location.
LatLng latlng=fromLocationToLatLng(location);
mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latlng,
17));
if(whereAmI!=null)
whereAmI.remove();
whereAmI=mMap.addMarker(new MarkerOptions().position(latlng).icon(BitmapDescriptorFactory.defaultMarker(
BitmapDescriptorFactory.HUE_GREEN)).title("Here I Am."));
[Snip code]
........
........
........
}
The Coventry app allows the user to interact with the map by placing markers on the map and then connecting up the markers with polylines drawn on the map. A polyline is a list of points, where line segments are drawn between consecutive points. The app detects long clicks on map and adds a marker. Lines can be drawn between markers using polylines. To do this the use clicks (do not long click) on a marker then moves to another marker and clicks. To remove the markers and lines click on the map.
In the image below the user has long clicked on three places on the map creating three markers. Then the user has clicked (just normal short click) on each point and the lines are drawn constructing a triangle around the vibrant metropolis of Coventry, England.
public class MainActivity extends Activity
implements OnMapClickListener, OnMapLongClickListener, OnMarkerClickListener{
final int RQS_GooglePlayServices = 1;
private GoogleMap myMap;
Location myLocation;
TextView tvLocInfo;
boolean markerClicked;
PolylineOptions rectOptions;
Polyline polyline;
static final LatLng COVENTRY = new LatLng(52.4081, -1.5106);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvLocInfo = (TextView)findViewById(R.id.locinfo);
FragmentManager myFragmentManager = getFragmentManager();
MapFragment myMapFragment
= (MapFragment)myFragmentManager.findFragmentById(R.id.map);
myMap = myMapFragment.getMap();
myMap.setMyLocationEnabled(true);
myMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
myMap.setOnMapClickListener(this);
myMap.setOnMapLongClickListener(this);
myMap.setOnMarkerClickListener(this);
//Move the camera instantly to the best city in the world! with a zoom of 15.
myMap.moveCamera(CameraUpdateFactory.newLatLngZoom(COVENTRY, 15));
myMap.animateCamera(CameraUpdateFactory.zoomTo(10), 2000, null);
markerClicked = false;
}
This code sets up and inflates the menu which is rendered. If the user selects the "legal notice" menu item a dialog with the licence is shown. Nothing new for us here. The dialog is constructed using a AlertDialog.Builder as normal.
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_legalnotices:
String LicenseInfo = GooglePlayServicesUtil.getOpenSourceSoftwareLicenseInfo(
getApplicationContext());
AlertDialog.Builder LicenseDialog = new AlertDialog.Builder(MainActivity.this);
LicenseDialog.setTitle("Legal Notices");
LicenseDialog.setMessage(LicenseInfo);
LicenseDialog.show();
return true;
}
return super.onOptionsItemSelected(item);
}
Before the app comes into focus the app checks if the map service is still available. If for example the app is pushed into the background the map needs to be updated and displayed again when it comes into focus. If the app can't "connect" to play services the app informs the user.
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(getApplicationContext());
if (resultCode == ConnectionResult.SUCCESS){
Toast.makeText(getApplicationContext(),
"isGooglePlayServicesAvailable SUCCESS",
Toast.LENGTH_LONG).show();
}else{
GooglePlayServicesUtil.getErrorDialog(resultCode, this, RQS_GooglePlayServices);
}
}
The user can move the map around and zoom in/out as they wish. The callbacks set up in onCreate() are shown below:
Let's look at the code more below.
@Override
public void onMapClick(LatLng point) {
tvLocInfo.setText(point.toString());
myMap.animateCamera(CameraUpdateFactory.newLatLng(point));
markerClicked = false;
}
@Override
public void onMapLongClick(LatLng point){
tvLocInfo.setText("New marker added@" + point.toString());
myMap.addMarker(new MarkerOptions().position(point).title(point.toString()));
markerClicked = false;
}
@Override
public boolean onMarkerClick(Marker marker){
if(markerClicked){
if(polyline != null){
polyline.remove();
polyline = null;
}
rectOptions.add(marker.getPosition());
rectOptions.color(Color.RED);
polyline = myMap.addPolyline(rectOptions);
}else{
if(polyline != null){
polyline.remove();
polyline = null;
}
rectOptions = new PolylineOptions().add(marker.getPosition());
markerClicked = true;
}
return true;
}
}
The logic for onMarkerClick() is straightforward. First time onMarkerClick() is called it will call new PolylineOptions().add(marker.getPosition()) to add the marker to the polyline. Next time it is called it will add the second marker --rectOptions.add(marker.getPosition()) -- and then draw the line. Each time a new marker is added (via a long click) and a line drawn -- the complete set of lines are redrawn starting at the first marker added to the rectOptions (i.e., the ployline). If the user clicks on the map (not the markers) and then clicks on any marker or does a long click to create a new marker then the line or polyline is remove - that is, the line between the markers is cleared.