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.

How to create the Map part in Compastic

There are two maps display in Compastic!

  1. The first one displays the current location and the place of interest location. Both points are connected with a great circle line.
  2. The second one is used when the user looks for a place of interest using a keyword. The results returned by the geoCoder are displayed on a map so that the user can choose which is the correct one.

Now in a javascript maps api, this would be a no brainer piece of work, 5 mns at most. With Android unfortunately things are not so simple.

To start with you will have to set up your Map API environment. There are already countless blogs on creating a map with Android. I found this one very good for learning the basics and getting quickly up to speed. I will not paraphrase this tutorial but focus on the specifics of Compastic that are not covered in the blog.

The specifics of Compastic! are basically:
- display a great circle line to connect the two points of 1)
- geo search based on a keyword for 2). I want the application to return a list of places based on keywords I enter. I want the application to display those in a list view and on the map with a little icon and label to match the list item.

In the next post I will explain how to display a great circle on a Map View.

Monday 11 July 2011

How to build the Compass View

As explained in the previous emails, we now have passed up to date information to the compass view to show us the direction of our place of interest.

We want the compass view to look like this:

Well actually, if you can make it look nicer, please advise!

In this screenshot, we have a north pointing up, but it is normally not. So the whole compass bitmap underneath will be rotated by the corresponding angle.

CompassView is actually a class on its own. It extends View and is merely a canvas to be painted on. We add CompassView to the main layout with the following tag:


<com.metaaps.mobile.compastic.widgets.CompassView
    android:id="@+id/compassview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
/>

The view itself is in fact contained in a ViewFlipper to be able to switch between Compass and Map view, on user request.

Now let's move on to the drawing of the canvas itself.

We have to draw the following layers:
  1. The Magnetic North direction using a bitmap of a compass
  2. The direction of the place of interest from where we are
  3. Additional information which include distance and current position data

In code that gives:

@Override protected void onDraw(Canvas canvas) {

     paint = new Paint();
     
        canvas.drawColor(Color.TRANSPARENT);

        paint.setAntiAlias(true);
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.FILL);

        // draw magnetic north direction
        drawCompass(canvas);
        // draw arrow to show direction to place of interest position
        drawArrow(canvas, currentPlace);
        drawCenter(canvas);
    }


To draw the MN direction we use the following code:

private void drawCompass(Canvas canvas) {
        int w = getWidth();
        int h = getHeight();
        int cx = w / 2;
        int cy = h / 2;
        
        if(canvasBitmap == null) {
         canvasBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.compass);
        }

     canvas.save();
        canvas.translate(cx, cy);
     // rotate to be aligned with the true north
        canvas.rotate(north);
        float maxwidth = (float) (canvasBitmap.getWidth() * Math.sqrt(2));
        float maxheight = (float) (canvasBitmap.getHeight() * Math.sqrt(2));
        float ratio = Math.min(w / maxwidth, h / maxheight);
        int width = (int) (canvasBitmap.getWidth() * ratio);
        int height = (int) (canvasBitmap.getHeight() * ratio);
        // draw the compass
        canvas.drawBitmap(canvasBitmap, new Rect(0, 0, canvasBitmap.getWidth(), canvasBitmap.getHeight()), new Rect(- width / 2, - height/2, width / 2, height / 2), paint);
     canvas.restore();
 }


The idea is to first center and rotate the bitmap by the Magnetic North direction and then paint the compass bitmap. The initial calculation is to make sure the painted bitmap will fit within the canvas dimensions.

Now we move on to the arrow itself. At this stage we are still missing the arrow angle in fact. To calculate the angle, we look at the bearing between the current location and the place of interest. This bearing is actually provided by the Location class itself using the bearingTo method. However the bearing will give you the angle with the true North and not with the Magnetic North. So we need to find out where is the True North first.
For this we need to add the Declination, as explained in an earlier post. The declination value is actually a function of the position and the altitude. The declination values can be found for instance at the NOAA website. I initially loaded all value from there and injected them in a large float table in my application. Declinations vary over time according to a model, so the whole thing meant I also had to update the code every year or so to compensate for the changes. It started to get a bit messy, until I found out about the GeomagneticField class.
This class has a getDeclination method. Initialised with the right lat, long, altitude and time of the day, it will give you the actual declination for any point on earth.
Once you get the declination for your current location you can find out the actual angle by adding all three values: magneticNorth + declination + bearing.

private void drawArrow(Canvas canvas, Place place) {
        int w = getWidth();
        int h = getHeight();
        int cx = w / 2;
        int cy = h / 2;

        if (currentLocation != null && currentPlace != null) {
                // generate the arrow path
         if(arrowPath == null) {
          int height = (int) (Math.min(w / 2, h / 2));
          int width = height / 5;
                        int arrowWidth = 2 * width;
                        int arrowHeight = arrowWidth / 2;
                        arrowPath = new Path();
                        // Construct an arrow path
                        arrowPath.moveTo(- width / 2, height / 3);
                        arrowPath.lineTo(width / 2, height / 3);
                        arrowPath.lineTo(width / 2, - height * 2 / 3);
   arrowPath.lineTo(arrowWidth / 2, - height * 2 / 3);
   arrowPath.lineTo(0, - height * 2 / 3 - arrowHeight);
                        arrowPath.lineTo(- arrowWidth / 2, - height * 2 / 3);
                        arrowPath.lineTo(- width / 2, - height * 2 / 3);
                        arrowPath.lineTo(- width / 2, height / 3);
                        arrowPath.close();
         }
         canvas.save();
                canvas.translate(cx, cy);
         // calculate bearing
         float bearing = currentLocation.bearingTo(place.getLocation());
         // now get the declination angle
         float declination = 0.0f;
      GeomagneticField geoMagneticField = new GeomagneticField((float) currentLocation.getLatitude(), (float) currentLocation.getLongitude(), (float) currentLocation.getAltitude(), new Date().getTime());
      declination = geoMagneticField.getDeclination();
         // calculate the new angle to the true north in degrees
         float MNBearing = (float) (north + declination + bearing);
         // rotate to be aligned with the true north
                canvas.rotate(MNBearing);
                // draw the arrow
                paint.setColor(Color.parseColor("#bdc0dc"));
                paint.setStyle(Style.FILL);
                canvas.drawPath(arrowPath, paint);
                paint.setColor(Color.BLACK);
                paint.setStyle(Style.STROKE);
                paint.setStrokeWidth(2);
                canvas.drawPath(arrowPath, paint);
                // display the distance
         float distance = currentLocation.distanceTo(place.getLocation());
         String distanceStr = "Distance: ";
         if(distance < 10000) {
          distanceStr += (int) distance + " m";
         } else {
          distanceStr += (int) (distance / 1000) + " km";
         }
         canvas.restore();
                // display current location
                Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
                paint.setTextSize(15);
                paint.setColor(Color.DKGRAY);
                paint.setTextAlign(Align.LEFT);
         canvas.drawText(distanceStr, 5, 25, paint);
                paint.setTextSize(15);
         canvas.drawText("Your Loc: (" + ((int) (currentLocation.getLatitude() * 100)) / 100.0 + "º, " + ((int) (currentLocation.getLongitude() * 100)) / 100.0 + "º, " + (int) currentLocation.getAltitude() + "m)", 5, h - 20, paint);
        } else if(currentLocation == null) {
                Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
                paint.setTextSize(15);
                paint.setColor(Color.RED);
                paint.setTextAlign(Align.LEFT);
         canvas.drawText("Current Location is Unknown!", 5, h - 20, paint);
        }
}

And that's it really for the compass view. In the next post I will talk about the Map view and how to get the Geocoder to find a place and its location.

How to use the magnetic and orientation sensors to build the compass

If you have read the previous posts, you will recall I want to display directions and distance from my current location to a place of interest.

I will need the following sensor information:

  • an idea of where I am, ie my current location in latitude and longitude as given by either my network location or, better, by the embarked GPS
  • the orientation of my phone with regards to the true North. Your phone will give you the Magnetic North direction but you can easily derive the true North by adding the Declination, see for instance this Wikipedia article.
To the two sensor information above I will add a third one:
  • the magnetic field value. This will help me find out if the orientation is not biased by some magnetic object around.
To collect up to date information from these three sensors, you will need to use the SensorManager class. You can register a listener to all supported sensors. Do not forget to unregister, sensor listening is very battery consuming.


listener = new SensorEventListener() {

         public void onSensorChanged(SensorEvent event) {
             float[] values = event.values;
             if (compassView != null) {
              compassView.setNorth(- values[0]);
              compassView.invalidate();
             }
         }

         public void onAccuracyChanged(Sensor sensor, int accuracy) {
         }
     };
     
    magneticFieldListener = new SensorEventListener() {

          public void onSensorChanged(SensorEvent event) {
            float[] values = event.values;
            if(values.length < 3) {
              return;
            }
            if(Math.sqrt(values[0] * values[0] + values[1] * values[1] + values[2] * values[2]) > SensorManager.MAGNETIC_FIELD_EARTH_MAX) {
              if(alertDisplayed == false) {
               alert(Compastic.this, "Abnormal magnetic field! check for magnets or iron objects around you and recalibrate the device by doing an '8 shape' pattern with your phone.");
               alertDisplayed = true;
              } else {
                if(toastDisplayed == false) {
                   toast(Compastic.this, "Abnormal magnetic field! ");
                   toastDisplayed = true;
                }
              }
            } else {
              // field back to normal
              toastDisplayed = false;
            }
         }

         public void onAccuracyChanged(Sensor sensor, int accuracy) {
         }
     };
     

Both listeners are fields so I can unregister them and register them again when the activity stops and resumes.

The orientation listener updates the compass view (more on that later) when the orientation has changed. The change event contains a 3 dimensional float vector. The only part we are interested in here is value[0] which represents the Magnetic North direction. We update the compass view drawing by changing the angle value and invalidating its content to force the refresh.

The Magnetic field listener is used to check the actual value of the field. The value in micro Tesla is calculated with the L2 norm. We know that in any case the field cannot be greater than the MAGNETIC_FIELD_EARTH_MAX. If it is then we display an alert dialog. For instance my phone comes with a case that holds a magnet, ie to close the cover. This Magnet is way stronger than the Earth field. If I move the magnet around the phone, I can see the arrow following the magnet.

For the location now, we use the LocationManager class. We decide to listen to both the GPS and the network location for location updates.

locationService.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 25, locationListener);
locationService.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 5000, 25, locationListener);
The location listener is basically handling the location changed event from the location service by updating the compass view with the new location.

We are missing one last parameter: the place of interest. If you remember the screenshots from the design post, there is a top bar that enables the user to select a place of interest. The top bar is made of a Spinner with its own adapter. I will come back onto that later on. Bottom line is you can choose the place of interest from that spinner. At init the place of interest is the first item in the spinner list. This place contains all necessary location information that we need to construct a Location object. The compass view is updated with this value at init and any time the place of interest changes.

To sum it up, the compass view itself has up to date information about the magnetic north angle and the current location of the phone as well as the place of interest. With all these parameters we can now display the compass graphics to the user.

The next post will brush the compass view display using the android Canvas and many other goodies.

Design Phase

I always start my application design with the various screens (or activities) and how the user navigates between them.

In Compastic! I need:
  1. the main screen with
    1. the place of interest selected and a way to change it
    2. a compass view showing the Magnetic North direction and the direction to the place of interest
    3. the distance to that place
    4. a way to switch mode as I also wanted to be able to display the locations (current and of interest) on a Map
  2. an "Add Place" screen where the user can add a new place of interest, with
    1. a search bar, to enter the name of the place in text
    2. a map to visualise the places geographically
    3. a list to select the place of choice
  3. a couple of standard dialogs for Welcoming and Instructions
Here's an overview of these three screens:



Like I said I am no professional Android developer and does this as a hobby. I could really do with some UI/UX advice!

In the next posts I will discuss the actual implementation of each screens.

Compastic!

There are millions of Compass applications out there on Android. They range from trivial to very elaborate, with 3D views and camera overlays. The market winner is clearly Compass by Catch.com with over 12 millions downloads.
I bet it has been on the market since its inception to get top of the "Compass" search list. It is a sad truth that the Android Market search ranking are highly correlated to the time you have been on the market. It's really hard to get started in a crowded area - unless you have a very viral concept/application like the all mighty "Angry Birds".

Compass is by all means a nice application, very professional. In my opinion it missed a little something though. I mean I have no real use of a compass, living most of the time in town or near town. However I like geography and I like knowing my bearings so I decided to add yet another application to the compass list of applications.
I named it "Compastic!" and its sole purpose is to tell you which direction to look at and how far are your places of interest.
Let's take two use cases:
1) I am in Paris and want to know where to look for the Tour Eiffel
2) I am lost in the forest and I want to find my way back to the car park.
I can a third one, a bit more geeky, I just want to know where and how far is San Francisco from my sofa?

That's for the basic concept. Compass doesn't do it, or at least is not that straightforward.
I skipped the Market Study phase. I didn't download the 100+ compass applications to see if one was actually providing this feature. I just decided that I could take a couple of days off my work and implement it.

I am no Android professional but I surely know my Java. I have released a few Android applications in the past. It's a hobby and WE work only. I do have quite a few more ideas, Mobile Application is such an exciting field, with countless possibilities.

Next post I will dive into the details of how I implemented Compastic!

By the way, my other apps:
Domino! (a prank, half a day to build, my very first one)
- Kids Stars (some pissed off customer, there was a bug, has left me with the dreaded 1 star, and a terrible rating)
- and Compastic! of course