Saturday, February 15, 2014

Introduction to Using Google Maps v2

    Maps are arguably one of the most versatile and useful components when used correctly in a mobile application. For this project, I wanted to go over the basics of how to use a map in a fragment, as well as go over some of the tools that make maps useful. As with my other projects, the source code that I will be going over is available online here.


    To start, we're going to want to edit our manifest to include the Google Maps API key. There's plenty of tutorials out there already for getting this key, including this one from the Google documentation, so for the sake of brevity we'll skip over that process and assume that it exists as a value in the strings.xml file. We will want to include this key within the applications tag in the manifest as a piece of meta-data using the name "com.google.android.maps.v2.API_KEY".

<meta-data
    android:name="com.google.android.maps.v2.API_KEY"
    android:value="@string/maps_api_key" />

    We're also going to want to put in our play services version number as meta-data between the closing tags for application and manifest using the name "com.google.android.gms.version".

<meta-data
    android:name="com.google.android.gms.version"
    android:value="@integer/google_play_services_version" />

    To finish up the manifest, the last thing we'll need is our set of permissions for the app, such as Internet access, location access, and optionally local storage permissions. While local storage is not a necessity for using maps, I used it for saving persistent information about the map.

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<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_WIFI_STATE" />
<uses-permission android:name="com.ptrprograms.maps.permission.MAPS_RECEIVE" />

   The general layout of this project consist of three classes; the main activity, the map fragment, and a listener. The listener interface consists of two methods and is used for communicating from the fragment to the main activity.

public interface mapListener {
    public void playServicesUnavailable();
    public void longClickedMap( LatLng latLng );
}

    Both of these methods are fairly straight forward. One is called when the map determines that Google Play Services are unavailable, making the map unusable, and the other is called when the map detects a long click in order to send that location to the main activity.

    The purpose of the main activity is to control the high level functions of the application while leaving the fragment as a general tool that can be manipulated easily by the main activity. For this demonstration, MainActivity simply loads in our map fragment, handles the case of there being no Play Services ( puts up a dialog that leads the user to downloading services or closing the application ), and responds to a long click on the map by requesting that a marker be placed in the location.

    The bulk of the work in this application is handled through our map fragment. While Google provides a map fragment that can be accessed and manipulated from an activity, I decided to build my own fragment and include the GoogleMap in order to add my own helper functionality. I do include two listeners pertaining to GooglePlayServices in order to determine if the connection has succeeded, allowing the fragment to initialize, or failed.

public class PTRMapFragment extends Fragment implements GooglePlayServicesClient.ConnectionCallbacks, GooglePlayServicesClient.OnConnectionFailedListener

    Once the service has connected, the map is configured to show road and building names, as well as satellite images for the locations. The onMarkerClick and onMapLongClick listeners are also initialized on the map at this point.

    The action I have chosen when the user holds down on a location in the map is to simply add a marker to that location. This is done from the interface function longClickedMap( LatLng ) from our implemented MapListener class, and MainActivity simply calls the fragment's addMarker( LatLng ) function. addMarker( LatLng ) uses the GeoCoder object to retrieve the address listed for the given coordinates and adds a maker on that location with the address as a title for the marker.

public void addMarker( LatLng latLng ) {
        if( latLng == null )
            return;

        Geocoder geocoder = new Geocoder( getActivity() );
        String address;
        try {
            address = geocoder.getFromLocation( latLng.latitude, latLng.longitude, 1 ).get( 0 ).getAddressLine( 0 );
        } catch( IOException e ) {
            address = "";
        }
        Log.e( TAG, address );
        addMarker( 0, latLng, address );
    }

    public void addMarker( float color, LatLng latLng, String title ) {
        if( mMap == null )
            mMap = ( (SupportMapFragment) getFragmentManager()
                    .findFragmentById( R.id.map ) )
                    .getMap();

        if( latLng == null || mMap == null )
            return;

        MarkerOptions markerOptions = new MarkerOptions().position( latLng );
        if( !title.isEmpty() )
            markerOptions.title( title );

        if( color == 0 )
            color = BitmapDescriptorFactory.HUE_RED;

        markerOptions.icon( BitmapDescriptorFactory.defaultMarker( color ) );
        Marker marker = mMap.addMarker( markerOptions );
        if( !markerLocations.contains( marker ) )
            markerLocations.add( marker );

        marker.showInfoWindow();
    }



    Another important aspect of the map fragment is the camera. While I set the values for the camera in the fragment, they can also be set as attributes in the layout xml file. The method that I use checks for a shared preference to retrieve values that may have been changed by the user, or uses default values in order to focus the camera on the user's location and set the tilt, zoom and bearing.

     private void setInitialCameraPosition() {
        double lng, lat;
        float tilt, bearing, zoom;

        SharedPreferences settings = getActivity().getSharedPreferences( EXTRAS_SHARED_PREFERENCES, 0 );
        lng = Double.longBitsToDouble( settings.getLong( SAVED_STATE_LONG, Double.doubleToLongBits( mLocationClient.getLastLocation().getLongitude() ) ) );
        lat = Double.longBitsToDouble( settings.getLong( SAVED_STATE_LAT, Double.doubleToLongBits( mLocationClient.getLastLocation().getLatitude() ) ) );
        zoom = settings.getFloat( SAVED_STATE_ZOOM, 17 );
        bearing = settings.getFloat( SAVED_STATE_BEARING, 0 );
        tilt = settings.getFloat( SAVED_STATE_TILT, 30 );

        CameraPosition cameraPosition = new CameraPosition.Builder()
                .target( new LatLng( lat, lng) )
                .zoom( zoom )
                .bearing( bearing )
                .tilt( tilt )
                .build();
        if( cameraPosition == null || mMap == null )
            return;
        mMap.animateCamera( CameraUpdateFactory.newCameraPosition( cameraPosition ) );
    }

    While I only implemented some map functionality, others include the ability to add circles, lines, closed polygons and overlays to make maps even more useful for applications. I also touched on the capabilities of Geocoder, but there's a lot more that can be utilized from that package in order find locations based on addresses or common names. I highly recommend going through the official documentation and seeing the full capabilities of these classes in order to assist your users to the fullest capacity.