Showing posts with label android compass java applications design. Show all posts
Showing posts with label android compass java applications design. 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

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.

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.