Advertisement

Android SDK: Working with Google Maps - Displaying Places of Interest

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

With Google Maps in your Android apps, you can provide users with localization functions, such as geographical information. Throughout this series we have been building an Android app in which the Google Maps Android API v2 combines with the Google Places API. So far we have displayed a map, in which the user can see their current location, and we have submitted a Google Places query to return data about nearby places of interest. This required setting up API access for both services. In the final part of the series, we will parse the Google Places JSON data and use it to show the user nearby places of interest. We will also make the app update the markers when the user location changes.

This is the last of four parts in a tutorial series on Using Google Maps and Google Places in Android apps:

App We Are Working Towards

This is a snapshot of the final app.

1. Process the Place Data

Step 1

You will need to add the following import statements to your Activity class for this tutorial:

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Log;

In the last tutorial we created an inner AsyncTask class to handle fetching the data from Google Places in the background. We added the doInBackground method to request and retrieve the data. Now we can implement the onPostExecute method to parse the JSON string returned from doInBackground, inside your AsyncTask class, after the doInBackground method:

protected void onPostExecute(String result) {
	//parse place data returned from Google Places
}

Step 2

Back in the second part of this series, we created a Marker object to indicate the user's last recorded location on the map. We are also going to use Markers to show the nearby places of interest. We will use an array to store these Markers. At the top of your Activity class declaration, add the following instance variable:

private Marker[] placeMarkers;

By default, the Google Places API returns a maximum of 20 places, so let's define this as a constant too:

private final int MAX_PLACES = 20;

When we create the Markers for each place, we will use MarkerOptions objects to configure the Marker details. Create another array instance variable for these:

private MarkerOptions[] places;

Now let's instantiate the array. In your Activity onCreate method, after the line in which we set the map type, create an array of the maximum required size:

placeMarkers = new Marker[MAX_PLACES];

Now let's turn to the onPostExecute method we created. First, loop through the Marker array, removing any existing Markers. This method will execute multiple times as the user changes location:

if(placeMarkers!=null){
	for(int pm=0; pm<placeMarkers.length; pm++){
		if(placeMarkers[pm]!=null)
			placeMarkers[pm].remove();
	}
}

When the app code first executes, new Markers will be created. However, when the user changes location, these methods will execute again to update the places displayed. For this reason the first thing we must do is remove any existing Markers from the map to prepare for creating a new batch.

Step 3

We will be using Java JSON resources to process the retrieved place data. Since these classes throw certain exceptions, we need to build in a level of error handling throughout this section. Start by adding try and catch blocks:

try {
	//parse JSON
}
catch (Exception e) {
	e.printStackTrace();
}

Inside the try block, create a new JSONObject and pass it to the result JSON string returned from doInBackground:

JSONObject resultObject = new JSONObject(result);

If you look at the Place Search page on the Google Places API documentation, you can see a sample of what the query actually returns in JSON. You will see that the places are contained within an array named "results". Let's first retrieve that array from the returned JSON object:

JSONArray placesArray = resultObject.getJSONArray("results");

You should refer to the sample JSON result as we complete each section of this process - keep the page open in a browser while you complete the remainder of the tutorial. Next let's instantiate the MarkerOptions array we created with the length of the returned "results" array:

places = new MarkerOptions[placesArray.length()];

This should give us a MarkerOptions object for each place returned. Add a loop to iterate through the array of places:

//loop through places
for (int p=0; p<placesArray.length(); p++) {
	//parse each place
}

Step 4

Now we can parse the data for each place returned. Inside the for loop, we will build details to pass to the MarkerOptions object for the current place. This will include latitude and longitude, place name, type and vicinity, which is an excerpt of the address data for the place. We will retrieve all of this data from the Google Places JSON, passing it to the Marker for the place via its MarkerOptions object. If any of the values are missing in the returned JSON feed, we will simply not display a Marker for that place, in case of Exceptions. To keep track of this, add a boolean flag:

boolean missingValue=false;

Now add local variables for each aspect of the place we need to retrieve and pass to the Marker:

LatLng placeLL=null;
String placeName="";
String vicinity="";
int currIcon = otherIcon;

We create and initialize a LatLng object for the latitude and longitude, strings for the place name and vicinity and initially set the icon to use the default icon drawable we created. Now we need another try block, so that we can detect whether any values are in fact missing:

try{
	//attempt to retrieve place data values
}
catch(JSONException jse){
	missingValue=true;
	jse.printStackTrace();
}

We set the missing value flag to true for checking later. Inside this try block, we can now attempt to retrieve the required values from the place data. Start by initializing the boolean flag to false, assuming that there are no missing values until we discover otherwise:

missingValue=false;

Now get the current object from the place array:

JSONObject placeObject = placesArray.getJSONObject(p);

If you look back at the sample Place Search data, you will see that each place section includes a "geometry" section which in turn contains a "location" section. This is where the latitude and longitude data for the place is, so retrieve it now:

JSONObject loc = placeObject.getJSONObject("geometry").getJSONObject("location");

Attempt to read the latitude and longitude data from this, referring to the "lat" and "lng" values in the JSON:

placeLL = new LatLng(
	Double.valueOf(loc.getString("lat")),
	Double.valueOf(loc.getString("lng")));

Next get the "types" array you can see in the JSON sample:

JSONArray types = placeObject.getJSONArray("types");

Tip: We know this is an array as it appears in the JSON feed surrounded by the "[" and "]" characters. We treat any other nested sections as JSON objects rather than arrays.

Loop through the type array:

for(int t=0; t<types.length(); t++){
	//what type is it
}

Get the type string:

String thisType=types.get(t).toString();

We are going to use particular icons for certain place types (food, bar and store) so add a conditional:

if(thisType.contains("food")){
	currIcon = foodIcon;
	break;
}
else if(thisType.contains("bar")){
	currIcon = drinkIcon;
	break;
}
else if(thisType.contains("store")){
	currIcon = shopIcon;
	break;
}

The type list for a place may actually contain more than one of these places, but for convenience we will simply use the first one encountered. If the list of types for a place does not contain any of these, we will leave it displaying the default icon. Remember that we specified these types in the Place Search URL query string last time:

food|bar|store|museum|art_gallery

This means that the only place types using the default icon will be museums or art galleries, as these are the only other types we asked for.

After the loop through the type array, retrieve the vicinity data:

vicinity = placeObject.getString("vicinity");

Finally, retrieve the place name:

placeName = placeObject.getString("name");

Step 5

After the catch block in which you set the missingValue flag to true, check that value and set the place MarkerOptions object to null, so that we don't attempt to instantiate any Marker objects with missing data:

if(missingValue)	places[p]=null;

Otherwise, we can create a MarkerOptions object at this position in the array:

else
	places[p]=new MarkerOptions()
	.position(placeLL)
	.title(placeName)
	.icon(BitmapDescriptorFactory.fromResource(currIcon))
	.snippet(vicinity);

Step 6

Now, at the end of onPostExecute after the outer try and catch blocks, loop through the array of MarkerOptions, instantiating a Marker for each, adding it to the map and storing a reference to it in the array we created:

if(places!=null && placeMarkers!=null){
	for(int p=0; p<places.length && p<placeMarkers.length; p++){
		//will be null if a value was missing
		if(places[p]!=null)
			placeMarkers[p]=theMap.addMarker(places[p]);
	}
}

Storing a reference to the Marker allows us to easily remove it when the places are updated, as we implemented at the beginning of the onPostExecute method. Notice that we include two conditional tests each time this loop iterates, in case the Place Search did not return the full 20 places. We also check in case the MarkerOptions is null, indicating that a value was missing.

Step 7

Finally, we can instantiate and execute our AsyncTask class. In your updatePlaces method, after the existing code in which we built the search query string, start this background processing to fetch the place data using that string:

new GetPlaces().execute(placesSearchStr);

You can run your app now to see it in action. It should display your last recorded location together with nearby places of interest. The colors you see on the Markers will depend on the places returned. Here is the app displaying a user location in Glasgow city center, UK:

App in Glasgow

Perhaps unsurprisingly a lot of the places listed in Glasgow are bars.

When the user taps a Marker, they will see the place name and snippet info:

Tapping Marker in Glasgow

2. Update With User Location Changes

Step 1

The app as it stands will execute once when it is launched. Let's build in the functionality required to make it update to reflect changes in the user location, refreshing the nearby place Markers at the same time.

Alter the opening line of the Activity class declaration to make it implement the LocationListener interface so that we can detect changes in the user location:

public class MyMapActivity extends Activity implements LocationListener {

A Location Listener can respond to various changes, each of which uses a dedicated method. Inside the Activity class, implement these methods:

@Override
public void onLocationChanged(Location location) {
	Log.v("MyMapActivity", "location changed");
	updatePlaces();
}
@Override
public void onProviderDisabled(String provider){
	Log.v("MyMapActivity", "provider disabled");
}
@Override
public void onProviderEnabled(String provider) {
	Log.v("MyMapActivity", "provider enabled");
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
	Log.v("MyMapActivity", "status changed");
}

The only one we are really interested in is the first, which indicates that the location has changed. In this case we call the updatePlaces method again. Otherwise we simply write out a Log message.

At the end of the updatePlaces method, add a request for the app to receive location updates:

locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 30000, 100, this);

We use the Location Manager we created earlier in the series, requesting updates using the network provider, at delays of 30 seconds (indicated in milliseconds), with a minimum location change of 100 meters and the Activity class itself to receive the updates. You can, of course, alter some of the parameters to suit your own needs.

Tip: Although the requestLocationUpdates method specifies a minimum time and distance for updates, in reality it can cause the onLocationChanged method to execute much more often, which has serious performance implications. In any apps you plan on releasing to users, you should therefore limit the frequency at which your code responds to these location updates. The alternative requestSingleUpdate method used on a timed basis may be worth considering.

Step 2

Last but not least, we need to take care of what happens when the app pauses and resumes. Override the two methods as follows:

@Override
protected void onResume() {
	super.onResume();
	if(theMap!=null){
		locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 30000, 100, this);
	}
}

@Override
protected void onPause() {
	super.onPause();
	if(theMap!=null){
		locMan.removeUpdates(this);
	}
}

We check for the GoogleMap object before attempting any processing, as in onCreate. If the app is pausing, we stop it from requesting location updates. If the app is resuming, we start requesting the updates again.

Tip: We've used the LocationManager.NETWORK_PROVIDER a few times in this series. If you are exploring localization functionality in your apps, check out the alternative getBestProvider method with which you can specify criteria for Android to choose a provider based on such factors as accuracy and speed.


Before We Finish

That pretty much completes the app! However, there are many aspects of the Google Maps Android API v2 that we have not even touched on. Once you have your app running you can experiment with features such as rotation and tilting. The updated maps service displays indoor and 3D maps in certain places. The following image shows the 3D facility with the app if the user location was in Venice, Italy:

3D Map

This has the map type set to normal - here is another view of Venice with the hybrid map type set:

Hybrid Map in Venice

Conclusion

In this tutorial series we have worked through the process of integrating both Google Maps and Google Places APIs in a single Android app. We handled API key access, setting up the development environment, workspace and application to use Google Play Services. We utilized location data, showing the user location together with nearby places of interest, and displaying the data with custom UI elements. Although what we have covered in this series is fairly extensive, it really is only the beginning when it comes to building localization features into Android apps. With the release of Version 2 of the Maps API, Android apps are set to take such functions to the next level.

Advertisement