Showing posts with label google maps. Show all posts
Showing posts with label google maps. Show all posts

Wednesday, 13 July 2011

How to search a list of places by keyword and display them with custom icons

What I need: let the user search for a place by name and display the search results obtained on a Map.

I won't go into the details of the layout and focus on the map aspect. The interface looks like this:


The user enters the place he is looking for and the application pulls available location matching the name, displays them in a list and on the map. To link the list and the icons items, numbers are used.
There are two challenges here:
1) get the list of places matching the name
2) render the icons on the map with a number

Getting the list of places is achieved thanks to GeoCoder with this simple snippet of code:

Geocoder geoCoder = new Geocoder(this, Locale.getDefault());
        try {
            List<Address> places = geoCoder.getFromLocationName(locationName, 10);
        } catch (IOException e) {                
            e.printStackTrace();
            Utils.alert(this, "Could not retrieve locations, do you have an Internet connection running?");
        }   


We need to add some code to fill the list and add the icons to the Map.

Geocoder geoCoder = new Geocoder(this, Locale.getDefault());
        try {
            List<Address> places = geoCoder.getFromLocationName(locationName, 10);
            placesList.setAdapter(new AddressesAdapter(this, places));
            // add places to the map
         // clear previous overlays
            List<Overlay> listOfOverlays = mapView.getOverlays();
            if(listOfOverlays != null) {
                listOfOverlays.clear();
                mapView.invalidate();
            }
            // add place markers
            ArrayList<GeoPoint> points = new ArrayList<GeoPoint>();
            int index = 0;
            for(Address address : places) {
                Marker marker = new Marker(this, address, index++);
                listOfOverlays.add(marker);
            }
        } catch (IOException e) {                
            e.printStackTrace();
            Utils.alert(this, "Could not retrieve locations, do you have an Internet connection running?");
        }   

We create our own adapter to display the places in the list. Essentially this adapter builds the name of the place and appends a number to it. To build the place name, use the following:

int length = addresses.get(position).getMaxAddressLineIndex();
     String add = position + ") ";
            for (int i = 0; i < length; i++) {
             String line = addresses.get(position).getAddressLine(i);
             if(line != null) {
                 add += line;
                 if(i != length - 1) {
                  add += ", ";
                 }
             }
            }
The markers are displayed by using our own Marker class that extends the Overlay class. The draw method is overriden to use our own bitmap and draw the index number in the middle of the canvas.
@Override
    public boolean draw(Canvas canvas, MapView mapView, boolean shadow, long when) 
    {
        super.draw(canvas, mapView, shadow);                   

        //---translate the GeoPoint to screen pixels---
        Point screenPts = new Point();
        mapView.getProjection().toPixels(this.geoPoint, screenPts);

        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setTextSize(bitmap.getHeight() / 2);
        paint.setColor(Color.DKGRAY);
        paint.setTextAlign(Align.CENTER);
        //---add the marker---
        canvas.drawBitmap(bitmap, screenPts.x - bitmap.getWidth() / 2, screenPts.y - bitmap.getHeight(), null);         
        canvas.drawText(index + "", screenPts.x, screenPts.y - bitmap.getHeight() / 2, paint);
        return true;
    }


One last word on testing. Just a small tip on how to test the location with the android avd. In Eclipse go to windows->open perspective->other->ddms. Go to the emulator control panel on the right and scroll down for location controls! Otherwise you can telnet your avd (while running) by using telnet localhost 5554, once logged in send the command geo fix 'yourlat' 'yourlong'.

This concludes the series on Compastic! Remember the full code is available @ http://code.google.com/p/compastic/ and the app is on the Android Market @ https://market.android.com/details?id=com.metaaps.mobile.compastic&feature=search_result

How to draw a great circle on a Map in Android

What I need: draw a great circle between two points on a Map.

Again in a web application using the Google Maps API this would be a no brainer, as polylines can be directly added to the map and include a great circle option. However this is unfortunately not the case with Android. Actually even drawing a straight line is more complicated. Let's start with a simple line then.

To display a line on the maps we will have to extend the Overlay Class and override the draw method. Drawing a line is quite trivial when you have your canvas. The issue here is to find out which coordinates to use for your points. Remember your points are expressed as Lat, Long coordinates, in degrees x 1E6 actually in the GeoPoint Map API. You will need to convert that to the map coordinate. Thankfully there are a couple of methods to help you achieve that goal in the MapView object.
Once you have you canvas coordinate points it is a pretty trivial task as shown in the small code below:

public class LineOverlay extends Overlay {

    private GeoPoint startGeoPoint;
    private GeoPoint stopGeoPoint;
    
    public LineOverlay(Context context, Location startLocation, Location stopLocation) {
     this.startGeoPoint = new GeoPoint((int) (startLocation.getLatitude() * 1E6), (int) (startLocation.getLongitude() * 1E6));
     this.stopGeoPoint = new GeoPoint((int) (stopLocation.getLatitude() * 1E6), (int) (stopLocation.getLongitude() * 1E6));
 }

 @Override
    public boolean draw(Canvas canvas, MapView mapView, boolean shadow, long when) 
    {
        super.draw(canvas, mapView, shadow);                   

        //---translate the GeoPoint to screen pixels---
        Point startScreenPts = new Point();
        mapView.getProjection().toPixels(this.startGeoPoint, startScreenPts);
        Point stopScreenPts = new Point();
        mapView.getProjection().toPixels(this.stopGeoPoint, stopScreenPts);
        
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(3);
        paint.setStrokeCap(Paint.Cap.ROUND);
        canvas.drawLine(startScreenPts.x, startScreenPts.y, stopScreenPts.x, stopScreenPts.y, paint);         
        return true;
    }

}

If I use the code above this is the line I get:



Which is not bad but not very correct from a geographical point of view. Enters the Great Circle and some calculations algorithm.

@Override
    public boolean draw(Canvas canvas, MapView mapView, boolean shadow, long when) 
    {
        super.draw(canvas, mapView, shadow);                   

        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(3);
        paint.setStrokeCap(Paint.Cap.ROUND);
        // get geodetic points
        List<GeoPoint> points = getGeodeticPoints(startGeoPoint, stopGeoPoint, 20);
        Point previousPoint = null;
        for(GeoPoint point : points) {
         // convert points to map coordinates
            Point mapPoint = new Point();
            mapView.getProjection().toPixels(point, mapPoint);
            if(previousPoint != null) {
                canvas.drawLine(previousPoint.x, previousPoint.y, mapPoint.x, mapPoint.y, paint);         
            }
            previousPoint = mapPoint;
        }

        return true;
    }

 static public List<GeoPoint> getGeodeticPoints(GeoPoint p1, GeoPoint p2, int numberpoints)
    {
                // adapted from page http://maps.forum.nu/gm_flight_path.html
  ArrayList<GeoPoint> fPoints = new ArrayList<GeoPoint>();
  // convert to radians
  double lat1 = (double) p1.getLatitudeE6() / 1E6 * Math.PI / 180;
  double lon1 = (double) p1.getLongitudeE6() / 1E6 * Math.PI / 180;
  double lat2 = (double) p2.getLatitudeE6() / 1E6 * Math.PI / 180;
  double lon2 = (double) p2.getLongitudeE6() / 1E6 * Math.PI / 180;

  double d = 2*Math.asin(Math.sqrt( Math.pow((Math.sin((lat1-lat2)/2)),2) + Math.cos(lat1)*Math.cos(lat2)*Math.pow((Math.sin((lon1-lon2)/2)),2)));
  double bearing = Math.atan2(Math.sin(lon1-lon2)*Math.cos(lat2), Math.cos(lat1)*Math.sin(lat2)-Math.sin(lat1)*Math.cos(lat2)*Math.cos(lon1-lon2))  / -(Math.PI/180);
  bearing = bearing < 0 ? 360 + bearing : bearing;

  for (int n = 0 ; n < numberpoints + 1; n++ ) {
   double f = (1.0/numberpoints) * n;
   double A = Math.sin((1-f)*d)/Math.sin(d);
   double B = Math.sin(f*d)/Math.sin(d);
   double x = A*Math.cos(lat1)*Math.cos(lon1) +  B*Math.cos(lat2)*Math.cos(lon2);
   double y = A*Math.cos(lat1)*Math.sin(lon1) +  B*Math.cos(lat2)*Math.sin(lon2);
   double z = A*Math.sin(lat1)           +  B*Math.sin(lat2);

   double latN = Math.atan2(z,Math.sqrt(Math.pow(x,2)+Math.pow(y,2)));
   double lonN = Math.atan2(y,x);
   fPoints.add(new GeoPoint((int) (latN/(Math.PI/180) * 1E6), (int) (lonN/(Math.PI/180) * 1E6)));
  }

        return fPoints;
 }

}



With the above code the line is now a great circle!


The great circle calculation algorithm was adapted to Java and Android from the http://maps.forum.nu/gm_flight_path.html blog page.

In the next posts I will touch the geocoder and the custom icon display side of the application.