Previous  | Next  | Home

Map Overlay Demo


 

In this project we shall extend our discussion of map overlays, learning how to

As in Mapping Demo, we shall in this project require use of the Google Maps API key to permit map tiles to be downloaded to the device.

 

Creating the Project

Create a new project in Eclipse (New > Android Project) using the following specifications:

where you should substitute your namespace for com.lightcone.

 

Filling out the Code

Edit strings.xml to add some strings that we will need:


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="hello">MapOverlayDemo</string>
    <string name="app_name">Map Overlay Demo</string>
    <string name="goLabel">Show the Map</string>
    <string name="overlay_label">Food</string>
    <string name="access_label">Access</string>
    <string name="route_label">Route</string>
</resources>

and edit main.xml to read


  <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:padding="10dip" >
            
        <Button 
            android:id="@+id/mapshow_button"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="@string/goLabel" />

    </LinearLayout>

which will lay out an initial screen with a single button.

Import the file ShowTheMap.java from the project com.lightcone.mappingdemo into the source directory (right-click on MapOverlayDemo/src/com.lightcone.mapoverlaydemo and select Import. On the resulting screen select General > File System and click Next, on the next screen click Browse, and then navigate to the workspace directory containing the file, click OK, select the file on the resulting page, and then click Finish.) There will be some red xs in Eclipse because we have to make some changes in this file. Open the imported file ShowTheMap.java and edit it to read (red indicates lines that have been added or changed relative to the imported file)



    package com.lightcone.mapoverlaydemo;

    import com.google.android.maps.GeoPoint;
    import com.google.android.maps.MapActivity;
    import com.google.android.maps.MapController;
    import com.google.android.maps.MapView;
    import android.os.Bundle;
    import android.view.KeyEvent;
    import android.view.Window;
 
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Button;

    
    public class ShowTheMap extends MapActivity {
                
        private static double lat;
        private static double lon;
        private int latE6;
        private int lonE6;
        private MapController mapControl;
        private GeoPoint gp;
        private MapView mapView;
        
        private Button overlayButton, accessButton;

        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            requestWindowFeature(Window.FEATURE_NO_TITLE);  // Suppress title bar for more space
            setContentView(R.layout.showthemap);
            
            // Add map controller with zoom controls
            mapView = (MapView) findViewById(R.id.mv);
            mapView.setSatellite(false);
            mapView.setTraffic(false);
            mapView.setBuiltInZoomControls(true);   // Set android:clickable=true in main.xml
 
            int maxZoom = mapView.getMaxZoomLevel();
            int initZoom = maxZoom-2;

            mapControl = mapView.getController();
            mapControl.setZoom(initZoom);
            // Convert lat/long in degrees into integers in microdegrees
            latE6 =  (int) (lat*1e6);
            lonE6 = (int) (lon*1e6);
            gp = new GeoPoint(latE6, lonE6);
            mapControl.animateTo(gp);    
 
            // Button to control food overlay
            overlayButton = (Button)findViewById(R.id.doOverlay);
            overlayButton.setOnClickListener(new OnClickListener(){      
                public void onClick(View v) {	
   
                }
            });
            
            // Button to control access overlay
            accessButton = (Button)findViewById(R.id.doAccess);
            accessButton.setOnClickListener(new OnClickListener(){      
                public void onClick(View v) {	
      
                }
            });

        }

        // Method to insert latitude and longitude in degrees
        public static void putLatLong(double latitude, double longitude){
            lat = latitude;
            lon =longitude;
        }
        
        // This sets the s key on the phone to toggle between satellite and map view
        // and the t key to toggle between traffic and no traffic view (traffic view
        // relevant only in urban areas where it is reported).
        
        public boolean onKeyDown(int keyCode, KeyEvent e){
            if(keyCode == KeyEvent.KEYCODE_S){
                mapView.setSatellite(!mapView.isSatellite());
                return true;
            } else if(keyCode == KeyEvent.KEYCODE_T){
                mapView.setTraffic(!mapView.isTraffic());
                mapControl.animateTo(gp);  // To ensure change displays immediately
            }
                return(super.onKeyDown(keyCode, e));
        }
                            
        // Required method since class extends MapActivity
        @Override
        protected boolean isRouteDisplayed() {
                return false;  // Don't display a route
        }
    }

The OverlayItem arrays foodItem and accessItem contain information about items that we are going to overlay on the map. The first argument of the OverlayItem constructor is the GeoPoint location for the overlay item, the second is its label of the overlay item, and the third is a short piece of information about the overlay item. Two buttons have been added to control the two sets of overlay items. The buttons invoke the methods setOverlay1 and setOverlay2, respectively, with the logic associated with the display described in the header comments for these methods.

Next create a file showthemap.xml in res/layout, selecting as the root element FrameLayout. Edit it to give the content


  <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    
        <view android:id="@+id/mv"
            class="com.google.android.maps.MapView"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_weight="1" 
            android:clickable="true"
            android:apiKey="07WVUg-srWUY6iEC2qTEiuT1mKYkoo6EVPK74pA"
            />
            
            <!-- Must replace apiKey above with appropriate one for your development machine  -->
                
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" 
            android:padding="0px"           
            >
                                                    
        <Button android:id="@+id/doOverlay"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30px"
            android:layout_weight="1.0" 
            android:textSize="12sp"   
            android:text="@string/overlay_label" />
            
        <Button android:id="@+id/doAccess"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1.0"
            android:textSize="12sp" 
            android:text="@string/access_label" />
            
        <Button android:id="@+id/doRoute"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginRight="30px"
            android:layout_weight="1.0"
            android:textSize="12sp" 
            android:text="@string/route_label" />
                        
        </LinearLayout>
                    
    </FrameLayout>

where you need to substitute for the apiKey in this file the value appropriate for your machine. Because the root node is a FrameLayout, this will lay out a map filling the screen, with three buttons superposed on the map centered at the top.

Open the file MapOverlayDemo.java, and edit it to add the content specified below in red


   package com.lightcone.mapoverlaydemo;
    
    import android.app.Activity;
    import android.os.Bundle;

    import android.content.Context;
    import android.content.Intent;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Toast;

    public class MapOverlayDemo extends Activity implements OnClickListener {
            
        static Context context;
            
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            
            // Get application context for later use
            context = getApplicationContext();
            
            // Add ClickListener for the button
            View firstButton = findViewById(R.id.mapshow_button);
            firstButton.setOnClickListener(this); 

        }
    
        @Override
        public void onClick(View v) {
            double lat = 35.955;
            double lon = -83.9265;
            switch(v.getId()){
                case R.id.mapshow_button:		
                    Intent i = new Intent(this, ShowTheMap.class);
                    ShowTheMap.putLatLong(lat, lon);
                    startActivity(i);
                    break;		
            }	
        }
            
        // Create a static method to show toasts (not presently used but included
        // as an example)
        
        public static void showToast(String text){
            Toast.makeText(context, text, Toast.LENGTH_LONG).show();
        }

    }

which implements an intent to launch ShowTheMap when the opening-screen button is pressed. Finally, add permissions to AndroidManifest.xml for internet access and the Google maps library, register an activity tag for the ShowTheMap class (and set debuggable to true if you are going to debug on a device):


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.lightcone.mapoverlaydemo"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-permission android:name="android.permission.INTERNET" />
    <application android:icon="@drawable/icon" android:label="@string/app_name" android:debuggable="true">
        <activity android:name=".MapOverlayDemo"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".ShowTheMap" android:label="Lat/Long Location"> </activity>
        <uses-library android:name="com.google.android.maps" />
    </application>
    <uses-sdk android:minSdkVersion="3" />
</manifest> 

Now if you execute this program you should get a screen with a single button and if you press that button you should get the screen in the following figure





(You can toggle the satellite view on and off by clicking "s" on the phone keyboard, toggle the traffic view with "t", and pan and zoom controls can be accessed by touching and dragging on the screen.)


Deliberately omit the <uses-library android:name="com.google.android.maps" /> tag from the manifest file and attempt to execute the program. You will get an error message that the app has stopped unexpectedly. If you consult the logcat output you will find something like the following error message:

06-22 22:05:46.769 W/dalvikvm( 6341): Unable to resolve superclass of
                                   Lcom/lightcone/mapoverlaydemo/ShowTheMap; (11)
06-22 22:05:46.769 W/dalvikvm( 6341): Link of class 'Lcom/lightcone/mapoverlaydemo/ShowTheMap;' failed
06-22 22:05:46.769 E/dalvikvm( 6341): Could not find class 'com.lightcone.mapoverlaydemo.ShowTheMap',
             referenced from method com.lightcone.mapoverlaydemo.MapOverlayDemo.

So the failure is because the virtual machine cannot find our class ShowTheMap, and that ultimately is because it cannot resolve its superclass. But we subclassed ShowTheMap from MapActivity, which is in the library com.google.android.maps (which, recall, is separate from the normal Android SDK). Thus it is clear why the superclass cannot be found and we suffer a fatal error. (Before you forget, restore the tag!).

If, you omit the <uses-permission android:name="android.permission.INTERNET" /> tag on the other hand, the program will run, but when you click the Show the Map button the screen that comes up will have buttons but no map (try it). That is, of course, because we must have an internet connection to download the map and Android requires that explicit permission be given to connect to the internet. No permission given, so no map.

Finally, omit <activity android:name=".ShowTheMap" android:label="Lat/Long Location"> </activity>. The program will execute the first screen but when you click the Show the Map button it will stop with a force close error message. In this case the Android runtime is smart enough to know exactly where we went astray, because we find in the logcat stream:

06-22 22:23:47.689 E/AndroidRuntime( 6420): android.content.ActivityNotFoundException: Unable to find
explicit activity class {com.lightcone.mapoverlaydemo/com.lightcone.mapoverlaydemo.ShowTheMap}; 
have you declared this activity in your AndroidManifest.xml?

Well, have you? Be sure to restore the AndroidManifest.xml file to its original form before proceeding.

 

Adding Map Overlays

Now we are going to use the buttons displayed on the map to trigger two kinds of actions illustrating uses of map overlays:

  1. A set of overlay icons indicating places of interest that can themselves be tapped to display further information, and

  2. display of a route between two points downloaded over the network in response to a query from our device

The first we shall implement by subclassing ItemizedOverlay<Item> (which is a subclass of com.google.android.maps.Overlay) and the second we shall implement by subclassing Overlay.

 

ItemizedOverlay

Let's import two image icons that we will need. Copy the images knifefork_small.png (image source) and accessibility.png (image source) from the course image resource page to a directory on your computer. Then in Eclipse right-click on MapOverlayDemo/res/drawable-hdpi and select Import. On the resulting screen, select General > File System and click Next. Click the Browse button and navigate to the directory containing two images. Click OK. Then on the resulting screen select the files to be imported, as in the following example





Once the files are selected, click Finish. The new image files knifefork_small.png and accessibility.png should now appear under res/drawable-hdpi.

Now create a new Java class MyItemizedOverlay by right-clicking on src/<package-name> and choosing New > Class. In the resulting dialog window specify Name: MyItemizedOverlay, Modifiers: public, Superclass: com.google.android.maps.ItemizedOverlay. Edit the resulting file MyItemizedOverlay.java to read


    package com.lightcone.mapoverlaydemo;
    
    import java.util.ArrayList;
    import android.graphics.drawable.Drawable;
    import android.widget.Toast;
    import com.google.android.maps.GeoPoint;
    import com.google.android.maps.ItemizedOverlay;
    import com.google.android.maps.OverlayItem;
    
    public class MyItemizedOverlay extends ItemizedOverlay<OverlayItem> {
            
        private ArrayList<OverlayItem> myOverlays ;
    
        public MyItemizedOverlay(Drawable defaultMarker) {
            super(boundCenterBottom(defaultMarker));
            myOverlays = new ArrayList<OverlayItem>();
            populate();
        }
            
        public void addOverlay(OverlayItem overlay){
            myOverlays.add(overlay);
            populate();
        }
    
        @Override
        protected OverlayItem createItem(int i) {
            return myOverlays.get(i);
        }
            
        // Removes overlay item i
        public void removeItem(int i){
            myOverlays.remove(i);
            populate();
        }
            
        // Handle tap events on overlay icons
        @Override
        protected boolean onTap(int i){
                
            /*	In this case we will just put a transient Toast message on the screen indicating that we have
            captured the relevant information about the overlay item.  In a more serious application one
            could replace the Toast with display of a customized view with the title, snippet text, and additional
            features like an embedded image, video, or sound, or links to additional information. (The lat and
            lon variables return the coordinates of the icon that was clicked, which could be used for custom
            positioning of a display view.)*/
            
            GeoPoint  gpoint = myOverlays.get(i).getPoint();
            double lat = gpoint.getLatitudeE6()/1e6;
            double lon = gpoint.getLongitudeE6()/1e6;
            String toast = "Title: "+myOverlays.get(i).getTitle();
            toast += "\nText: "+myOverlays.get(i).getSnippet();
            toast += 	"\nSymbol coordinates: Lat = "+lat+" Lon = "+lon+" (microdegrees)";
            Toast.makeText(MapOverlayDemo.context, toast, Toast.LENGTH_LONG).show();
            return(true);
        }
    
        // Returns present number of items in list
        @Override
        public int size() {
            return myOverlays.size();
        }
    }

The overlays in myOverlays are objects of the ArrayList class.

Finally, edit ShowTheMap.java to read (the red lines indicate the changes)


    package com.lightcone.mapoverlaydemo;

    import java.util.List;

    import com.google.android.maps.GeoPoint;
    import com.google.android.maps.MapActivity;
    import com.google.android.maps.MapController;
    import com.google.android.maps.MapView;
    import android.os.Bundle;
    import android.view.KeyEvent;
    import android.view.Window;
 
    import com.google.android.maps.Overlay;
    import com.google.android.maps.OverlayItem;
    import android.graphics.drawable.Drawable;

    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Button;

    
    public class ShowTheMap extends MapActivity {
                
        private static double lat;
        private static double lon;
        private int latE6;
        private int lonE6;
        private MapController mapControl;
        private GeoPoint gp;
        private MapView mapView;
        private Button overlayButton, accessButton;
 
        private List<Overlay> mapOverlays;
        private Drawable drawable1, drawable2;
        private MyItemizedOverlay itemizedOverlay1, itemizedOverlay2;
        private boolean foodIsDisplayed = false;
        
        // Define an array containing the food overlay items
        
        private OverlayItem [] foodItem = {
            new OverlayItem( new GeoPoint(35952967,-83929158), "Food Title 1", "Food snippet 1"), 
            new OverlayItem( new GeoPoint(35953000,-83928000), "Food Title 2", "Food snippet 2"),
            new OverlayItem( new GeoPoint(35955000,-83929158), "Food Title 3", "Food snippet 3") 
        };
        
        // Define an array containing the  access overlay items
        
        private OverlayItem [] accessItem = {
            new OverlayItem( new GeoPoint(35953700,-83926158), "Access Title 1", "Access snippet 1"),
            new OverlayItem( new GeoPoint(35954000,-83928200), "Access Title 2", "Access snippet 2"),
            new OverlayItem( new GeoPoint(35955000,-83927558), "Access Title 3", "Access snippet 3"),
            new OverlayItem( new GeoPoint(35954000,-83927158), "Access Title 4", "Access snippet 4") 
        };

    
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            requestWindowFeature(Window.FEATURE_NO_TITLE);  // Suppress title bar for more space
            setContentView(R.layout.showthemap);
            
            // Add map controller with zoom controls
            mapView = (MapView) findViewById(R.id.mv);
            mapView.setSatellite(false);
            mapView.setTraffic(false);
            mapView.setBuiltInZoomControls(true);   // Set android:clickable=true in main.xml 
            int maxZoom = mapView.getMaxZoomLevel();
            int initZoom = maxZoom-2;
            mapControl = mapView.getController();
            mapControl.setZoom(initZoom);
            // Convert lat/long in degrees into integers in microdegrees
            latE6 =  (int) (lat*1e6);
            lonE6 = (int) (lon*1e6);
            gp = new GeoPoint(latE6, lonE6);
            mapControl.animateTo(gp);    

            // Button to control food overlay
            overlayButton = (Button)findViewById(R.id.doOverlay);
            overlayButton.setOnClickListener(new OnClickListener(){      
                public void onClick(View v) {	
                     setOverlay1();     
                }
            });
            
            // Button to control access overlay
            accessButton = (Button)findViewById(R.id.doAccess);
            accessButton.setOnClickListener(new OnClickListener(){      
                public void onClick(View v) {	
                     setOverlay2();       
                }
            });

        }
        

        /* Methods to set map overlays. In this case we will place a small overlay image at
        a specified location. Place the marker image as a png file in res > drawable-* .
        For example, the reference to R.drawable.knifefork_small below is to an
        image file called knifefork_small.png in the project folder res > drawable-hdpi.
        Can only use lower case letters a-z, numbers 0-9, ., and _ in these image file names. 
        In this example the single overlay item is specified by drawable and the
        location of the overlay item is specified by overlayitem. */
        
        // Display food location overlay.  If not already displayed, clicking button displays all 
        // food overlays.  If already, displayed successive clicks remove items one by one. This
        // illustrates ability to remove individual overlay items dynamically at runtime.
        
        public void setOverlay1(){	
            int foodLength = foodItem.length;
            // Create itemizedOverlay2 if it doesn't exist and display all three items
            if(! foodIsDisplayed){
            mapOverlays = mapView.getOverlays();	
            drawable1 = this.getResources().getDrawable(R.drawable.knifefork_small); 
            itemizedOverlay1 = new MyItemizedOverlay(drawable1); 
            // Display all three items at once
            for(int i=0; i<foodLength; i++){
                itemizedOverlay1.addOverlay(foodItem[i]);
            }
            mapOverlays.add(itemizedOverlay1);
            foodIsDisplayed = !foodIsDisplayed;
            // Remove each item successively with button clicks
            } else {			
                itemizedOverlay1.removeItem(itemizedOverlay1.size()-1);
                if((itemizedOverlay1.size() < 1))  foodIsDisplayed = false;
            }    
            // Added symbols will be displayed when map is redrawn so force redraw now
            mapView.postInvalidate(); 
        }
        
        // Display accessibility overlay.  If not already displayed, successive button clicks display each of
        // the three icons successively, then the next removes them all.  This illustrates the ability to add
        // individual overlay items dynamically at runtime
        
        public void setOverlay2(){	
            int accessLength = accessItem.length;
            // Create itemizedOverlay2 if it doesn't exist
            if(itemizedOverlay2 == null ){
            mapOverlays = mapView.getOverlays();	
            drawable2 = this.getResources().getDrawable(R.drawable.accessibility);
            itemizedOverlay2 = new MyItemizedOverlay(drawable2);
            }     
            // Add items with each click
            if(itemizedOverlay2.size() < accessLength){
                    itemizedOverlay2.addOverlay(accessItem[itemizedOverlay2.size()]); 	
                    mapOverlays.add(itemizedOverlay2);      
            // Remove all items with one click
            } else {
                for(int i=0; i<accessLength; i++){
                    itemizedOverlay2.removeItem(accessLength-1-i);
                }
            }     
            // Added symbols will be displayed when map is redrawn so force redraw now
            mapView.postInvalidate();
        }

        // Method to insert latitude and longitude in degrees
        public static void putLatLong(double latitude, double longitude){
            lat = latitude;
            lon =longitude;
        }
        
        // This sets the s key on the phone to toggle between satellite and map view
        // and the t key to toggle between traffic and no traffic view (traffic view
        // relevant only in urban areas where it is reported).
        
        public boolean onKeyDown(int keyCode, KeyEvent e){
            if(keyCode == KeyEvent.KEYCODE_S){
                mapView.setSatellite(!mapView.isSatellite());
                return true;
            } else if(keyCode == KeyEvent.KEYCODE_T){
                mapView.setTraffic(!mapView.isTraffic());
                mapControl.animateTo(gp);  // To ensure change displays immediately
            }
                return(super.onKeyDown(keyCode, e));
        }
                            
        // Required method since class extends MapActivity
        @Override
        protected boolean isRouteDisplayed() {
                return false;  // Don't display a route
        }
    }

If you execute the app, click Show the Map, and then click the Food button the knife-fork symbols should display at the latitude/longitude coordinates assigned, and if you click the Access button the accessibility icons should display at the assigned coordinates. In these two cases we have implemented a little logic to illustrate the ability to change these icon overlays on the fly at runtime: The three food location icons all appear at once when you click Food the first time, and successive clicks of Food cause them to disappear one at a time, while the accessibility icons appear one at a time as you successively click Access, and then all disappear on the next click after all four icons are displayed. The following figure shows all icons displayed in the two overlays.




These simple exercises suggest possibilities for more significant applications: for example, a map that shows the location of traffic accidents or significant weather events, with the arrays containing the overlay items and their associated positions and information changing in response to those events, leading to real-time updates of the map display.

 

onTap() Events

We would usually want to know something further about what the overlay icons signify. The onTap event handler in MyItemizedOverlay causes a tap on an overlay icon to display information indicating which icon was tapped and additional information associated with it. In this case we will just put a transient Toast message on the screen indicating that we have captured the relevant information about the overlay item (the following figure illustrates a Toast produced by tapping on the rightmost accessibility icon.)

.

In a more serious application, one could replace the Toast with display of a customized view with the title, snippet text, and additional features like an embedded image, video, or sound, or links to further information. (The lat and lon variables return the coordinates of the icon that was clicked, which could be used for custom positioning of a display view near the icon that was tapped.)

 

Overlaying a Custom Route

Let us finally demonstrate how to access a server, retrieve a custom route between two points generated in XML format by programs running on the server, and plot the route on the map. We shall further assume that the route returned may contain additional information such as the local slope of the route (which could be important if one is negotiating the route in a wheelchair, for example). Since this requires a network access that could block the main UI thread if there are network difficulties, we shall also use this example to introduce the important skill of putting time-consuming or potential UI-blocking tasks on a background thread.

We shall use AsyncTask to run the process in the background. To use AsyncTask we must subclass it. First, create a new class RouteSegmentOverlay (in the file RouteSegmentOverlay.java in the src directory) that extends com.google.android.maps.Overlay:


    package com.lightcone.mapoverlaydemo;
    
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.graphics.Point;
    
    import com.google.android.maps.GeoPoint;
    import com.google.android.maps.MapView;
    import com.google.android.maps.Overlay;
    
    public class RouteSegmentOverlay extends Overlay {
        private GeoPoint locpoint;
        private Paint paint;
        private GeoPoint routePoints [];
        private int routeGrade[];
        private boolean routeIsActive;	
        private Point pold, pnew, pp;
        private int numberRoutePoints;
        
        // Constructor permitting the route array to be passed as an argument.
        public RouteSegmentOverlay(GeoPoint[] routePoints, int[] routeGrade) {
                this.routePoints = routePoints;
                this.routeGrade = routeGrade;
                numberRoutePoints  = routePoints.length;
                routeIsActive = true;
                // If first time, set initial location to start of route
                locpoint = routePoints[0];
                pold = new Point(0, 0);
                pnew = new Point(0,0);
                pp = new Point(0,0);
                paint = new Paint();
        }
        
        // Method to turn route display on and off
        public void setRouteView(boolean routeIsActive){
                this.routeIsActive = routeIsActive;
        }
    
        @Override
        public void draw(Canvas canvas, MapView mapview, boolean shadow) {
            super.draw(canvas, mapview, shadow);
            if(! routeIsActive) return;
            
            /* 
            The object mapview is a MapView.  The class MapView, documented at 
            http://code.google.com/android/add-ons/google-apis/reference/com/google/android/maps/MapView.html,
            has a method getProjection() that returns a Projection of the MapView.  Projection is an interface, docs at
            http://code.google.com/android/add-ons/google-apis/reference/com/google/android/maps/Projection.html,
            which has a method toPixels(GeoPoint in, android.graphics.Point out), which takes a GeoPoint (in) and outputs
            a Point (out) that contains the screen x and y coordinates (in pixels) corresponding to the GeoPoint. (with latitude
            and longitude specified in microdegrees).  Thus, after implementing mapview.getProjection().toPixels(gp, pp),
            where gp is a GeoPoint, pp.x and pp.y hold the corresponding screen coordinates for the current MapView.
            Note that this translation must be done each time draw is called, since the relationship between GeoPoints and
            x-y coordinates may have changed (for example, if the map were panned or zoomed since the last draw).
                */
            
            mapview.getProjection().toPixels(locpoint, pp);       // Converts GeoPoint to screen pixels
            
            int xoff = 0;
            int yoff = 0;
            int oldx = pp.x;
            int oldy = pp.y;
            int newx = oldx + xoff;
            int newy = oldy + yoff;
            
            paint.setAntiAlias(true);
    
            // Draw route segment by segment, setting color and width of segment according to the slope
            // information returned from the server for the route.
            
            for(int i=0; i<numberRoutePoints-1; i++){
                switch(routeGrade[i]){
                    case 1:
                            paint.setARGB(100,255,0,0);
                            paint.setStrokeWidth(3);
                            break;
                    case 2:
                            paint.setARGB(100, 0, 255, 0);
                            paint.setStrokeWidth(5);
                            break;
                    case 3:
                            paint.setARGB(100, 0, 0, 255);
                            paint.setStrokeWidth(7);
                            break;
                    case 4:
                            paint.setARGB(90, 153, 102, 153);
                            paint.setStrokeWidth(6);
                            break;
                }
                
                // Find endpoints of this segment in pixels
                mapview.getProjection().toPixels(routePoints[i], pold);
                oldx = pold.x;
                oldy = pold.y;
                mapview.getProjection().toPixels(routePoints[i+1], pnew);
                newx = pnew.x;
                newy = pnew.y;
                
                // Draw the segment
                canvas.drawLine(oldx, oldy, newx, newy, paint);
            }      
        }
    }

Then add to ShowTheMap the imports


    import android.util.Log;
    import java.net.URL;
    import java.net.MalformedURLException;
    import android.os.AsyncTask;
    import org.xmlpull.v1.XmlPullParser;
    import org.xmlpull.v1.XmlPullParserFactory;

and the class variables


    private Button routeButton;
    String TAG = "GPStest";
    // Set up the array of GeoPoints defining the route
    int numberRoutePoints;	
    GeoPoint routePoints [];   // Dimension will be set in class RouteLoader below
    int routeGrade [];               // Index for slope of route from point i to point i+1
    RouteSegmentOverlay route;   // This will hold the route segments
    boolean routeIsDisplayed = false;

Add INSIDE the class ShowTheMap a new private class RouteLoader, with android.os.AsyncTask as the superclass:


    // NOTE: This is added inside the class ShowTheMap in the file ShowTheMap.java

    /* Class to implement single task on background thread without having to manage
    the threads directly. Launch with "new RouteLoader().execute(new URL(urlString)". 
    Must be launched from the UI thread and may only be invoked once.  Adapted from 
    example in Ch.10 of Android Wireless Application Development. Use this to do data 
    load from network on separate thread from main user interface to prevent locking
    main UI if there is network delay. */
	
    private class RouteLoader extends AsyncTask<URL, String, String> {

        @Override
        protected String doInBackground(URL... params) {
            // This pattern takes more than one param but we'll just use the first
            try {
                URL text = params[0];

                XmlPullParserFactory parserCreator;

                parserCreator = XmlPullParserFactory.newInstance();

                XmlPullParser parser = parserCreator.newPullParser();

                parser.setInput(text.openStream(), null);

                publishProgress("Parsing XML...");

                int parserEvent = parser.getEventType();
                int pointCounter = -1;
                int wptCounter = -1;
                int totalWaypoints = -1;
                int lat = -1;
                int lon = -1;
                String wptDescription = "";
                int grade = -1;
                
                // Parse the XML returned on the network
                while (parserEvent != XmlPullParser.END_DOCUMENT) {
                    switch (parserEvent) {
                    case XmlPullParser.START_TAG:
                        String tag = parser.getName();
                        if(tag.compareTo("number")==0){
                            numberRoutePoints = Integer.parseInt(parser.getAttributeValue(null,"numpoints"));
                            totalWaypoints = Integer.parseInt(parser.getAttributeValue(null,"numwpts"));
                            routePoints = new GeoPoint[numberRoutePoints]; 
                            routeGrade = new int[numberRoutePoints];
                            Log.i(TAG, "   Total points = "+numberRoutePoints
                                            +" Total waypoints = "+totalWaypoints);
                        }
                        if(tag.compareTo("trkpt")==0){
                            pointCounter ++;
                            lat = Integer.parseInt(parser.getAttributeValue(null,"lat"));
                            lon = Integer.parseInt(parser.getAttributeValue(null,"lon"));
                            grade = Integer.parseInt(parser.getAttributeValue(null,"grade"));
                            routePoints[pointCounter] = new GeoPoint(lat, lon);
                            routeGrade[pointCounter] = grade;
                            Log.i(TAG,"   trackpoint="+pointCounter+" latitude="+lat+" longitude="+lon
                                                +" grade="+grade);
                        } else if(tag.compareTo("wpt")==0) {
                            wptCounter ++;
                            lat = Integer.parseInt(parser.getAttributeValue(null,"lat"));
                            lon = Integer.parseInt(parser.getAttributeValue(null,"lon"));
                            wptDescription = parser.getAttributeValue(null,"description");
                            Log.i(TAG,"   waypoint="+wptCounter+" latitude="+lat+" longitude="+lon
                                            +" "+wptDescription);                      	
                        } 
                        break;
                    }

                    parserEvent = parser.next();
                }

            } catch (Exception e) {
                Log.i("RouteLoader", "Failed in parsing XML", e);
                return "Finished with failure.";
            }

            return "Done...";
        }

        protected void onCancelled() {
            Log.i("RouteLoader", "GetRoute task Cancelled");
        }

        // Now that route data are loaded, execute the method to overlay the route on the map
        protected void onPostExecute(String result) {
                Log.i(TAG, "Route data transfer complete");
                overlayRoute();
        }

        protected void onPreExecute() {
            Log.i(TAG,"Ready to load URL");
        }

        protected void onProgressUpdate(String... values) {
            super.onProgressUpdate(values);
        }

    }

Also add to ShowTheMap the new method overlayRoute(),


    // Overlay a route.  This method is only executed after loadRouteData() completes
    // on background thread.
    
    public void overlayRoute() {
    	if(route != null) return;  // To prevent multiple route instances if key toggled rapidly
    	// Set up the overlay controller
    	route = new RouteSegmentOverlay(routePoints, routeGrade); // My class defining route overlay
    	mapOverlays = mapView.getOverlays();
    	mapOverlays.add(route);
    	
    	// Added symbols will be displayed when map is redrawn so force redraw now
        mapView.postInvalidate(); 
    }

and the new method loadRouteData():


    // Method to read route data from server as XML
    
    public void loadRouteData(){
    	try {
            String url = "http://eagle.phys.utk.edu/reubendb/UTRoute.php";
            String data = "?lat1=35952967&lon1=-83929158&lat2=35956567&lon2=-83925450";
            //RouteLoader RL = new RouteLoader();
            //RL.execute(new URL(url+data));
            new RouteLoader().execute(new URL(url+data));
        } catch (MalformedURLException e) {
            Log.i("NETWORK", "Failed to generate valid URL");
        }
    }

and inside the onCreate method of ShowTheMap add the button handler


    // Button to control route overlay
    routeButton = (Button)findViewById(R.id.doRoute);
    routeButton.setOnClickListener(new OnClickListener(){      
        public void onClick(View v) {	
            if(! routeIsDisplayed){
                routeIsDisplayed = true;
                loadRouteData();   				
            } else {
                if(route != null) route.setRouteView(false);
		route=null; // Prevent multiple route instances if key toggled rapidly
                routeIsDisplayed = false;
                mapView.postInvalidate(); 
            }
        }
    });

The XML defining the sample route that is returned by the server corresponds to


<?xml version="1.0" encoding="utf-8" standalone="yes"?>
  <number numpoints="23" numwpts="2"></number>
  <wpt lat="35952967" lon="-83929158" description="Construction"></wpt>
  <wpt lat="35955038" lon="-83929126" description="Heavy traffic"></wpt>
  <trk>
    <trkseg>
        <trkpt lat="35952967" lon="-83929158" grade="1"></trkpt>
        <trkpt lat="35954021" lon="-83930341" grade="1"></trkpt>
        <trkpt lat="35954951" lon="-83929075" grade="1"></trkpt>

        <trkpt lat="35955038" lon="-83929126" grade="4"></trkpt>
        <trkpt lat="35955203" lon="-83928973" grade="1"></trkpt>
        <trkpt lat="35955212" lon="-83928855" grade="1"></trkpt>
        <trkpt lat="35955603" lon="-83928273" grade="2"></trkpt>
        <trkpt lat="35955807" lon="-83928369" grade="1"></trkpt>
        <trkpt lat="35955974" lon="-83927943" grade="1"></trkpt>
        <trkpt lat="35956063" lon="-83927720" grade="1"></trkpt>
        <trkpt lat="35956291" lon="-83927358" grade="1"></trkpt>
        <trkpt lat="35956471" lon="-83927229" grade="1"></trkpt>

        <trkpt lat="35956541" lon="-83927176" grade="2"></trkpt>
        <trkpt lat="35956397" lon="-83927044" grade="3"></trkpt>
        <trkpt lat="35956274" lon="-83926685" grade="1"></trkpt>
        <trkpt lat="35956213" lon="-83926642" grade="1"></trkpt>
        <trkpt lat="35956239" lon="-83926261" grade="1"></trkpt>
        <trkpt lat="35956202" lon="-83925722" grade="1"></trkpt>
        <trkpt lat="35956226" lon="-83925467" grade="1"></trkpt>
        <trkpt lat="35956343" lon="-83925502" grade="1"></trkpt>
        <trkpt lat="35956324" lon="-83925617" grade="1"></trkpt>

        <trkpt lat="35956445" lon="-83925379" grade="1"></trkpt>
        <trkpt lat="35956567" lon="-83925450" grade="1"></trkpt>
    </trkseg>
  </trk>

which is available in the file sampleRouteXML.xml, or by pasting the address


http://eagle.phys.utk.edu/reubendb/UTRoute.php?lat1=35952967&lon1=-83929158&lat2=35956567&lon2=-83925450

into a browser and then viewing the page source of the page returned by the browser (in either case the browser display will probably give an error, but look at the page source).

The parsing of the XML stream is handled primarily by XmlPullParser and XmlPullParserFactory, with the parsing API documented and usage example given at the XML Pull Parsing site. These are used to parse the incoming XML describing the route and put the information defining the route into arrays routePoints[] and routeGrade[]. The route is then drawn segment by segment using a loop over these arrays in the draw method of RouteSegmentOverlay.

The object mapview is a MapView. The class MapView has a method getProjection() that returns a Projection of the MapView. Projection is an interface that has a method toPixels(GeoPoint in, android.graphics.Point out), which takes a GeoPoint and outputs a Point (out) that contains the screen x and y coordinates (in pixels) corresponding to the GeoPoint (with latitude and longitude specified in microdegrees). Thus, after implementing mapview.getProjection().toPixels(gp, pp), where gp is a GeoPoint, pp.x and pp.y hold the corresponding screen coordinates for the current MapView. Note that this translation must be done each time draw is called, since the relationship between GeoPoints and x-y coordinates may have changed (for example, if the map were panned or zoomed since the last draw).


Notice in RouteSegmentOverlay the implementation of a general principle that we should do as little as possible within the draw method because the system calls it whenever it decides the screen needs to be redrawn. For example, if the route is displayed and you pan the map by dragging on it with your finger, the draw method is called many times to redraw the screen as the map moves. The device will generally be more responsive if it has to do less in each redraw.

If you now execute the app and display the map, clicking on the Route button will display the route loaded from the server, as illustrated in the following figure.



Clicking Route again will toggle the route display off. The route displayed is color-coded for slope, which is part of the information contained in the data sent by the server in response to the query from the device.


The complete project for the application described above is archived at the link MapOverlayDemo



Exercises

1. Change the transient Toasts that display when overlay icons are tapped to customized views of your choice that display the title, snippet text, embed the overlay icon in the upper left corner of the displayed page as a logo, a (simulated) image of the location, and a (simulated) web link to a location with further information. Format this nicely and give the user a button to dismiss the view.

2. In the section on overlaying a custom route there are <wpt></wpt> tags in the XML transmitted over the network that carry information associated with cautions near the route ("heavy traffic" or "construction"). Modify the app to place that information as an overlay on the map at the appropriate locations.


Previous  | Next  | Home