Home > Android, Maps, Programming > MapApp5 : MapView and Activity

MapApp5 : MapView and Activity

Welcome to the fifth and final part of my tutorial on how to create a map app for Android without using Google™ APIs :).

Series outline:

______________________________________

So up until now we didn’t see any results :(, this is where everything changes :D.

In this part we’ll write three classes:

MapView: a custom view to render and manipulate the map.

MapAppActivity: the main (and only) activity for the app, mainly creates the MapView and handles activity state changes.

MapViewLocationListener : An extended LocationListener that knows how to deal with a MapView.


MapView:

First thing you should know about this view is that it contains two gps locations, one for the actual phone gps location and the other one for the view center. The first is to draw a marker the the users position and the second is to freely move\drag the map, so the longitude,latitude coordinates of the pixel at the center of the view is represented by the second gps location. If we want to enable auto follow we just make the second gps location match the first one, this way the user’s location will always be at the center of the view.

Let’s start with the fields and the constructor of MapView:

package com.mapapp.views;

import java.util.Collection;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.location.Location;
import android.view.MotionEvent;
import android.view.View;

import com.mapapp.helpers.PointD;
import com.mapapp.tileManagement.Tile;
import com.mapapp.tileManagement.TilesManager;
import com.mapapp.tileManagement.TilesProvider;

public class MapView extends View
{
	// Needed to pass to View constructor
	protected Context context;

	// MapView dimensions
	protected int viewWidth, viewHeight;

	// Provides us with tiles
	protected TilesProvider tileProvider;

	// Handles calculations
	protected TilesManager tileManager;

	// Different paints
	protected Paint fontPaint;
	protected Paint bitmapPaint = new Paint();
	protected Paint circlePaint = new Paint();

	// The location of the view center in longitude, latitude
	protected PointD seekLocation = new PointD(0, 0);
	// Location of the phone using Gps data
	protected Location gpsLocation = null;
	// If true then seekLocation will always match gpsLocation
	protected boolean autoFollow = false;

	// An image to draw at the phone's position
	protected Bitmap positionMarker;

	// touch position values kept for panning\dragging
	protected PointD lastTouchPos = new PointD(-1, -1);

	public MapView(Context context, int viewWidth, int viewHeight, TilesProvider tilesProvider, Bitmap positionMarker)
	{
		super(context);
		this.context = context;

		// Tiles provider is passed not created.
		// The idea is to hide the actual tiles source from the view
		// This way the view doesn't care whether the source is a database or the internet
		this.tileProvider = tilesProvider;

		// These values will be used later
		this.viewWidth = viewWidth;
		this.viewHeight = viewHeight;

		// Get the marker image
		this.positionMarker = positionMarker;

		// Creating a TilesManager assuming that the tile size is 256*256.
		// You might want to pass tile size as a parameter or even calculate it somehow
		tileManager = new TilesManager(256, viewWidth, viewHeight);

		// Initializes paints
		initPaints();

		// Fetching tiles from the tilesProvider
		fetchTiles();
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
	{
		// Setting width,height that was passed in the constructor as the view's dimensions
		setMeasuredDimension(viewWidth, viewHeight);
	}

The code is well commented I guess, since we’re extending a View we must override onMeasere and call
setMeasuredDimension inside it passing the dimensions from the constructors. This is to ensure that the view has the desired width and height.

Now to initPaints and fetchTiles:

	void initPaints()
	{
		// Font paint is used to draw text
		fontPaint = new Paint();
		fontPaint.setColor(Color.DKGRAY);
		fontPaint.setShadowLayer(1, 1, 1, Color.BLACK);
		fontPaint.setTextSize(20);

		// Used to draw a semi-transparent circle at the phone's gps location
		circlePaint.setARGB(70, 170, 170, 80);
		circlePaint.setAntiAlias(true);
	}

	void fetchTiles()
	{
		// Update tilesManager to have the center of the view as its location
		tileManager.setLocation(seekLocation.x, seekLocation.y);

		// Get the visible tiles indices as a Rect
		Rect visibleRegion = tileManager.getVisibleRegion();

		// Tell tiles provider what tiles we need and which zoom level.
		// The tiles will be stored inside the tilesProvider.
		// We can get those tiles later when drawing the view
		tileProvider.fetchTiles(visibleRegion, tileManager.getZoom());
	}

Hopefully you remember how TilesManager works, it takes a location and returns the range of tiles needed to fill the MapView, so we first set the location of the TilesManager to the center of our MapView (Longitude,Latitude coordinates) then we get the result as a rectangle, then we pass this rectangle to the TilesProvider which fetches all the tiles included in the rectangle and stores them in a Hashtable inside itself.

Next thing is drawing the map, since we’re extending a View we override onDraw to draw out view contents, onDraw takes a Canvas object as parameter, the canvas object is used to draw text\images\shapes on the view.

The problem here is how to figure out the right position to draw the tiles. It’s easy if we were drawing the full world map a, we just convert longitude & latitude to pixels and draw them on the map.

In our case we are only drawing part of the world map and we need to have seekLocation at the center of the MapView, so in onDraw we calculate an offset value and pass it to all other drawing functions.

here’s the code for that and I guess it’s well commented, an image after the code should explain more.

	@Override
	protected void onDraw(Canvas canvas)
	{
		// Clear the view to grey
		canvas.drawARGB(255, 100, 100, 100);

		// To draw the map we need to find the position of the pixel representing the center of the view.
		// We need the position to be relative to the full world map, lets call this pixel position "pix"
		// pix.x will range from 0 to (2^zoom)*tileSize-1, same for pix.y
		// To draw anything on the map we subtract pix from the original position
		// It's just like dragging the map so that the pixel representing the gps location gets into the center of the view

		// In a square world map,
		// we need to know pix location as two values from 0.0 to 1.0
		PointD pixRatio = TilesManager.calcRatio(seekLocation.x, seekLocation.y);

		// Full world map width in pixels
		int mapWidth = tileManager.mapSize() * 256;
		Point pix = new Point((int) (pixRatio.x * mapWidth), (int) (pixRatio.y * mapWidth));

		/*
		 * Subtracting pix from each tile position will result in pix being drawn at the top left corner of the view
		 * To drag it to the center we add (viewWidth/2, viewHeight/2) to the final result
		 * pos.x = pos.x - pix.x + viewWidth/2f
		 * pos.x = pox.x - (pix.x - viewWidth/2f)
		 * ---> offset.x =  (pix.x - viewWidth/2f)
		 * same for offset.y
		 */

		Point offset = new Point((int) (pix.x - viewWidth / 2f), (int) (pix.y - viewHeight / 2f));
		// offset is now ready to use

		// Drawing tiles in a separate function to make the code more readable
		drawTiles(canvas, offset);

		// Draw the marker that pinpoints the user's location
		drawMarker(canvas, offset);
	}
Calculating offset

Top left: we drag the map to make the marker have the position of (0,0) in MapView
Top right : we need to center the marker in MapView
Bottom left : the final result we want



So to draw anything in the map we first obtain its position in the full world map then subtract offset from that position, for tiles we just multiply the index of the tile with tile width in pixels, so tiles x positions are like: 0, 256, 512, 1024… since in our case tileWidth is equal to 256.

	void drawTiles(Canvas canvas, Point offset)
	{
		// Get tiles from the Hashtable inside tilesProvider
		Collection tilesList = tileProvider.getTiles().values();

		// x,y are the calculated offset

		// Go trough all the available tiles
		for (Tile tile : tilesList)
		{
			// We act as if we're drawing a map of the whole world at a specific
			// zoom level
			// The top left corner of the map occupies the pixel (0,0) of the
			// view
			int tileSize = tileManager.getTileSize();
			long tileX = tile.x * tileSize;
			long tileY = tile.y * tileSize;

			// Subtract offset from the previous calculations
			long finalX = tileX - offset.x;
			long finalY = tileY - offset.y;

			// Draw the bitmap of the tiles using a simple paint
			canvas.drawBitmap(tile.img, finalX, finalY, bitmapPaint);
		}
	}

Important note: to draw a bitmap we used the simplest paint we can create, using other paints will slow down drawing!.

For the marker it’s the same, we just convert longitude latitude to pixels and subtract offset:

	void drawMarker(Canvas canvas, Point offset)
	{
		// x,y are the calculated offset

		// Proceed only if a gps fix is available
		if (gpsLocation != null)
		{
			// Get marker position in pixels as if we're going to draw it on a
			// world map where the top left corner of the map occupies the (0,0)
			// pixel of the view
			Point markerPos = tileManager.lonLatToPixelXY(gpsLocation.getLongitude(), gpsLocation.getLatitude());

			// Add offset to the marker position
			int markerX = markerPos.x - offset.x;
			int markerY = markerPos.y - offset.y;

			// Draw the marker and make sure you draw the center of the marker
			// at the marker location
			canvas.drawBitmap(positionMarker, markerX - positionMarker.getWidth() / 2, markerY - positionMarker.getHeight() / 2,
					bitmapPaint);

			// Around the marker we will draw a circle representing the accuracy of the gps fix
			// We first calculate its radius

			// Calculate how many meters one pixel represents
			float ground = (float) tileManager.calcGroundResolution(gpsLocation.getLatitude());

			// Location.getAccuracy() returns the accuracy in meters.
			float rad = gpsLocation.getAccuracy() / ground;

			canvas.drawCircle(markerX, markerY, rad, circlePaint);

			// Just drawing location info
			int pen = 1;
			canvas.drawText("lon:" + gpsLocation.getLongitude(), 0, 20 * pen++, fontPaint);
			canvas.drawText("lat:" + gpsLocation.getLatitude(), 0, 20 * pen++, fontPaint);
			canvas.drawText("alt:" + gpsLocation.getAltitude(), 0, 20 * pen++, fontPaint);
			canvas.drawText("Zoom:" + tileManager.getZoom(), 0, 20 * pen++, fontPaint);
		}
	}

We’re done now with the drawing part, we need to enable the user to drag the map:

Update 28/10/2012: Instead of reinventing the wheel there’s a class called GestureDetector that can detect many touch actions like clicking, double clicking, scrolling, I didn’t update the code here because that will break the connection with the post comments, so here’s the better implementation of MapView: MapView.java

and here’s the original unmodified code 🙂 :

	@Override
	public boolean onTouchEvent(MotionEvent event)
	{
		int action = event.getAction();

		if (action == MotionEvent.ACTION_DOWN)
		{
			// Keep touch position for later use (dragging)
			lastTouchPos.x = (int) event.getX();
			lastTouchPos.y = (int) event.getY();

			return true;
		}
		else if (action == MotionEvent.ACTION_MOVE)
		{
			autoFollow = false;

			PointD current = new PointD(event.getX(), event.getY());

			// Find how many pixels the users finger moved in both x and y
			PointD diff = new PointD(current.x - lastTouchPos.x, current.y - lastTouchPos.y);

			// In a full wolrd map, get the position of the center of the view in pixels
			Point pixels1 = tileManager.lonLatToPixelXY(seekLocation.x, seekLocation.y);

			// Subtract diff from that position
			Point pixels2 = new Point(pixels1.x - (int) diff.x, pixels1.y - (int) diff.y);

			// Reconvert the final result to longitude, latitude point
			PointD newSeek = tileManager.pixelXYToLonLat((int) pixels2.x, (int) pixels2.y);

			// Finally move the center of the view to the new location
			seekLocation = newSeek;

			// Refresh the view
			fetchTiles();
			invalidate(); // Causes the view to redraw itself

			// Prepare for the next drag event
			lastTouchPos.x = current.x;
			lastTouchPos.y = current.y;

			return true;
		}

		return super.onTouchEvent(event);
	}

The MapView class is almost finished, we will just add some small functions, the name should explain what each one does:

	// Fetch the tiles then draw, don't call to often
	public void refresh()
	{
		fetchTiles();
		invalidate();
	}

	// Like refresh but called from a non UI thread
	public void postRefresh()
	{
		fetchTiles();
		postInvalidate();
	}

	// Simply sets seek location to gpsLocation (if exists)
	public void followMarker()
	{
		if (gpsLocation != null)
		{
			seekLocation.x = gpsLocation.getLongitude();
			seekLocation.y = gpsLocation.getLatitude();
			autoFollow = true;

			fetchTiles();
			invalidate();
		}
	}

	public void zoomIn()
	{
		tileManager.zoomIn();
		onMapZoomChanged();
	}

	public void zoomOut()
	{
		tileManager.zoomOut();
		onMapZoomChanged();
	}

	protected void onMapZoomChanged()
	{
		tileProvider.clear();
		fetchTiles();
		invalidate();
	}

	// Returns the gps coordinates of the user
	public Location getGpsLocation()
	{
		return gpsLocation;
	}

	// Returns the gps coordinates of our view center
	public PointD getSeekLocation()
	{
		return seekLocation;
	}

	// Centers the given gps coordinates in our view
	public void setSeekLocation(double longitude, double latitude)
	{
		seekLocation.x = longitude;
		seekLocation.y = latitude;
	}

	// Sets the marker position
	public void setGpsLocation(Location location)
	{
		setGpsLocation(location.getLongitude(), location.getLatitude(), location.getAltitude(), location.getAccuracy());
	}

	// Sets the marker position
	public void setGpsLocation(double longitude, double latitude, double altitude, float accuracy)
	{
		if (gpsLocation == null) gpsLocation = new Location("");
		gpsLocation.setLongitude(longitude);
		gpsLocation.setLatitude(latitude);
		gpsLocation.setAltitude(altitude);
		gpsLocation.setAccuracy(accuracy);

		if (autoFollow) followMarker();

	}

	public int getZoom()
	{
		return tileManager.getZoom();
	}

	public void setZoom(int zoom)
	{
		tileManager.setZoom(zoom);
		onMapZoomChanged();
	}

Okay, that’s all for the MapView class, so this tutorial is almost over.


MapViewLocationListener:

a LocationListener is an interface that has a function we are interested in, it’s called

onLocationChanged(Location location);

Our location listener will implement this interface and whenever a change in location happens it will set the new location as the marker position in the MapView associated with this listener.
The full class code:

package com.mapapp.views;

import android.location.Location;
import android.location.LocationListener;
import android.os.Bundle;

public class MapViewLocationListener implements LocationListener
{
	MapView mapView;
	boolean stopped = false;

	public MapViewLocationListener(MapView mapView)
	{
		this.mapView = mapView;
	}

	@Override
	public void onLocationChanged(Location location)
	{
		if (!stopped && location != null)
		{
			// Set location and update the mapView
			mapView.setGpsLocation(location.getLongitude(), location.getLatitude(), location.getAltitude(), location.getAccuracy());
			mapView.postInvalidate();
		}
	}

	public void stop()
	{
		stopped = true;
		mapView = null;
	}

	@Override
	public void onProviderDisabled(String provider)
	{
	}

	@Override
	public void onProviderEnabled(String provider)
	{
	}

	@Override
	public void onStatusChanged(String provider, int status, Bundle extras)
	{
	}

}



MapAppActivity:

Very simple activity, it creates TilesProvider then creates a MapView passing the TilesProvider in the constructor, then the MapViewLocationListener is created and the MapView is passed in the constructor, what makes the class longer is adding activity state management, you don’t want the app to be reset when the user rotates his mobile, you also want the app to preserve the location in the map and the zoom level when another activity comes in front of your app. To do so we’ll override two functions in the activity: onSaveInstanceState, onRestoreInstanceState.

We also want the app to keep the seek location and zoom level of the MapView when the user closes the app and re opens it, to do so we will use the SharedPreferences.

Just one important note, I used onResume to initialize the app instead of on create for a simple reason, in the method onPause I’m releasing all the resources of the app, so I had to reload them when the activity starts again, the problem with onCreate is that it’s not always called, there are scenarios where the activity is started with the function onResume.

If you follow this link and scroll down to figure1 you see that an arrow goes from onPause (Where we unload our resources) directly to onResume, onCreate wasn’t called.

package com.mapapp.main;

import android.app.Activity;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Environment;
import android.view.Display;
import android.view.KeyEvent;

import com.mapapp.helpers.PointD;
import com.mapapp.mapapp.R;
import com.mapapp.tileManagement.TilesProvider;
import com.mapapp.views.MapView;
import com.mapapp.views.MapViewLocationListener;

public class MapAppActivity extends Activity
{

	// Constant strings used in onSaveInstanceState, onRestoreInstanceState
	private final class Save
	{
		public final static String GPS_LON = "gpsLon";
		public final static String GPS_LAT = "gpsLAT";
		public final static String GPS_ALT = "gpsALT";
		public final static String GPS_ACC = "gpsAcc";
	}

	// Constant strings to save settings in SharedPreferences
	// Also used for restoring settings
	private final class Pref
	{
		public final static String SEEK_LON = "seek_lon";
		public final static String SEEK_LAT = "seek_lat";
		public final static String ZOOM = "zoom";
	}

	// Our only view, created in code
	MapView mapView;

	// Provides us with Tiles objects, passed to MapView
	TilesProvider tilesProvider;

	// Updates marker location in MapView
	MapViewLocationListener locationListener;

	Location savedGpsLocation;

	@Override
	protected void onResume()
	{
		// Create MapView
		initViews();

		// Restore zoom and location data for the MapView
		restoreMapViewSettings();

		// Creating and registering the location listener
		locationListener = new MapViewLocationListener(mapView);
		LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
		locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListener);

		// Set our MapView as the main view for the activity
		setContentView(mapView);

		// Never ever forget this 🙂
		super.onResume();
	}

	void initViews()
	{
		// Creating the bitmap of the marker from the resources
		Bitmap marker = BitmapFactory.decodeResource(getResources(), R.drawable.marker);

		// Creating our database tilesProvider to pass it to our MapView
		String path = Environment.getExternalStorageDirectory() + "/mapapp/world.sqlitedb";
		tilesProvider = new TilesProvider(path);

		// Creating the mapView and make sure it fills the screen
		Display display = getWindowManager().getDefaultDisplay();
		mapView = new MapView(this, display.getWidth(), display.getHeight(), tilesProvider, marker);

		// If a location was saved while pausing the app then use it.
		if (savedGpsLocation != null) mapView.setGpsLocation(savedGpsLocation);

		// Update and draw the map view
		mapView.refresh();
	}

	@Override
	protected void onPause()
	{
		// Save settings before leaving
		saveMapViewSettings();

		// Mainly releases the MapView pointer inside the listener
		locationListener.stop();

		// Unregistering our listener
		LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
		locationManager.removeUpdates(locationListener);

		// Closes the source of the tiles (Database in our case)
		tilesProvider.close();
		// Clears the tiles held in the tilesProvider
		tilesProvider.clear();

		// Release mapView pointer
		mapView = null;

		super.onPause();
	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event)
	{
		// Zooming
		if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_Z)
		{
			mapView.zoomIn();
			return true;
		}
		// Zooming
		else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_X)
		{
			mapView.zoomOut();
			return true;
		}
		// Enable auto follow
		if (keyCode == KeyEvent.KEYCODE_H || keyCode == KeyEvent.KEYCODE_FOCUS)
		{
			mapView.followMarker();
			return true;
		}
		// Simulate being at some location, for testing only
		else if (keyCode == KeyEvent.KEYCODE_M || keyCode == KeyEvent.KEYCODE_MENU)
		{
			mapView.setGpsLocation(46.142578, -20.841015, 0, 182);
			mapView.invalidate();

			return false;
		}

		return super.onKeyDown(keyCode, event);
	}

	// Called manually to restore settings from SharedPreferences
	void restoreMapViewSettings()
	{
		SharedPreferences pref = getSharedPreferences("View_Settings", MODE_PRIVATE);

		double lon, lat;
		int zoom;

		lon = Double.parseDouble(pref.getString(Pref.SEEK_LON, "0"));
		lat = Double.parseDouble(pref.getString(Pref.SEEK_LAT, "0"));
		zoom = pref.getInt(Pref.ZOOM, 0);

		mapView.setSeekLocation(lon, lat);
		mapView.setZoom(zoom);
		mapView.refresh();
	}

	// Called manually to save settings in SharedPreferences
	void saveMapViewSettings()
	{
		SharedPreferences.Editor editor = getSharedPreferences("View_Settings", MODE_PRIVATE).edit();

		PointD seekLocation = mapView.getSeekLocation();
		editor.putString(Pref.SEEK_LON, Double.toString(seekLocation.x));
		editor.putString(Pref.SEEK_LAT, Double.toString(seekLocation.y));
		editor.putInt(Pref.ZOOM, mapView.getZoom());

		editor.commit();
	}

	@Override
	protected void onSaveInstanceState(Bundle outState)
	{
		if (mapView.getGpsLocation() != null)
		{
			outState.putDouble(Save.GPS_LON, mapView.getGpsLocation().getLongitude());
			outState.putDouble(Save.GPS_LAT, mapView.getGpsLocation().getLatitude());
			outState.putDouble(Save.GPS_ALT, mapView.getGpsLocation().getAltitude());
			outState.putFloat(Save.GPS_ACC, mapView.getGpsLocation().getAccuracy());
		}

		super.onSaveInstanceState(outState);
	}

	@Override
	protected void onRestoreInstanceState(Bundle savedInstanceState)
	{
		double gpsLon, gpsLat, gpsAlt;
		float gpsAcc;

		gpsLon = savedInstanceState.getDouble(Save.GPS_LON, 999);
		gpsLat = savedInstanceState.getDouble(Save.GPS_LAT, 999);
		gpsAlt = savedInstanceState.getDouble(Save.GPS_ALT, 999);
		gpsAcc = savedInstanceState.getFloat(Save.GPS_ACC, 999);

		if (gpsLon != 999 && gpsLat != 999 && gpsAlt != 999 && gpsAcc != 999)
		{
			savedGpsLocation = new Location(LocationManager.GPS_PROVIDER);
			savedGpsLocation.setLongitude(gpsLon);
			savedGpsLocation.setLatitude(gpsLat);
			savedGpsLocation.setAltitude(gpsAlt);
			savedGpsLocation.setAccuracy(gpsAcc);
		}

		super.onRestoreInstanceState(savedInstanceState);
	}
}

Last things you need to do:

  1. add this image to your drawable foldersmarker
  2. add this permission to your AndroidManifest.xml<uses-permission android:name=”android.permission.ACCESS_FINE_LOCATION” />
  3. copy the world.sqlite database to /sdcard/mapapp/

The app should be working fine by now :D.

Here’s the final source code for the app : MapApp_Final Source Code.zip

And here’s the final Apk file: MapApp_Final.apk

I really hope you learned something from this tutorial 😀

It feels good to have your own app that displays a map and a location 😀

Please feel free to have any suggestions or notes about this tutorial since it’s my first on in Android programming :).

Want to support online maps? head to MapApp6: Web support :D.!

Categories: Android, Maps, Programming Tags: , ,
  1. 09/06/2012 at 8:53 am

    I want to download the map as sqlitedb foramt.please help me.
    hlaingwintunn@gmail.com

    • Fuchs
      09/06/2012 at 3:43 pm

      There’s a project called Mobile Atlas Creator that can do that and more
      http://mobac.sourceforge.net/

      You can find lots of tutorials on the internet.
      Just make sure you don’t download large areas or you will be blocked by maps servers.
      Also make sure you read the the usage policy provided by the server you’re downloading the tiles from.

  2. 09/06/2012 at 8:58 am

    I want to moidfy this app.
    Can I add point ,lines and polygon on this mapview?And then
    I want to display extra information with popup box.

    • Fuchs
      09/06/2012 at 3:59 pm

      You can do that with ease, the map tiles are drawn onto a Canvas object, A Canvas has many draw methods so basically you can draw anything after the map is drawn.
      You can make a class called for example MapOverlay that defines some special drawing methods, then in the MapView define an ArrayList of MapOverlays.
      After you draw the map you loop through all the MapOverlays and draw them on the map.
      Here’s a quick code:

      public class MapOverlay {
          public void draw(Canvas canvas){
              canvas.draw**
          }
      
      public class POIOverlay extends MapOverlay {
       // override draw
      }
      
      public class SomeOverlayClass extends MapOverlay {
       // override draw
      }
      

      in MapView:

      ArrayList overlays = new ArrayList();
      // add overlays of some kind
      
      void onDraw(Canvas canvas) {
         // draw map
      
         // loop through all MapOverlays and call overlay.draw(canvas)
      }
      

      Hope that helps 🙂

  3. 12/06/2012 at 9:25 am

    Thank you for your answer.

  4. 12/06/2012 at 1:57 pm

    Thank you for your answer.
    I have a tiles map(sqlitedb format).So I want to use my map instead of world map. My map have a tiles table with four fields(id TEXT,s int ,t int ,image BLOB).
    id s t image

    BDCACCACD 0 0 PNG
    BDCACCABC 0 0 PNG
    …………. etc

    How do I work?
    How to use this map instead of world map?Please sample code….
    How to know maxlatitude,minlatitude,maxlongitude,minlongitude and radius for my map?
    My map is yangon city map.I want to use this map on my android application.
    Please help me.

    • Fuchs
      13/06/2012 at 5:18 pm

      Your map is also divided into tiles but the indexing is different, If you can download the map again using MobileAtlasCreator and choose the output format “RMaps format” the map will work fine without changing the code (Highly recommended), if that’s not possible you will need to know the following:

      The length of the index indicates the zoom level, for example “BDCACCACD” is a tile at the zoom level 9
      So in each zoom level you have four tiles:
      ABCD Tiles System
      The index of the green tile above should be AD
      If you want to use the same code in the tutorial you’ll have to convert from one indexing method to another,
      First let’s agree that A=”00″ (no x or y offset), B=”10″ (offset only in x), C=”01″ (offset in y), D=”11″ (offset in both x and y)
      let’s say you have the following tile T(x,y,z)=(11,5,3) and you want to convert it to the ABCD format, first you convert x,y to binary representation with z+1 digits (four digits here)
      Bin(x) = 1011
      Bin(y) = 0101
      Now if we merge two digits that have the index i from the binary representation
      i0 = Dec(“10“) = B
      i1 = Dec(“01“) = C
      i2 = Dec(“10“) = B
      i3 = Dec(“11“) = D

      The final index we need is BCBD, that’s what you should query for in the database,
      About the min\max coordinates it’s not easy to get these values and it won’t even be accurate, my guess is that you don’t need those values, just treat the map you have as any other map since the quad tree will take care of organizing the tiles for you.

      Sorry for the delay in my response but it’s college exams week :(.

      • 09/08/2014 at 12:04 pm

        hi Fuchs, I found out my location using https://www.openstreetmap.org website…
        Please take a look at this screenshot:

        I export directly from there. But it seems not as “RMaps format” you just said. Can we use it?

        And another thing i found,
        My location on that map seems not very detail.
        Logically if I go out of the building and turning on my GPS,
        of course the coordinates would be updated each time i move,
        right? So although the map is not clearly detail, the red-pin (marker) showing where I am still could put above that map right?

        • 09/08/2014 at 12:05 pm

          sorry i forgot to put the screenshot,
          Here please:

  5. 14/06/2012 at 3:07 pm

    Thank you for your reply.
    I download the map as sqlitedb format using MobileAtlasCreator.Downloaded map is similar world map in this tutorial.So I used this map instead of world map because of your guideline.
    So thank you very much.But ,when I start run the application ,the map is display smaller than the screen size.
    I want to display the map with full screen.
    How to modify or repair in this tutorial?
    please help me.

    • Fuchs
      14/06/2012 at 4:48 pm

      1-If you are in zoom level 1 or 2 the map might not be large enough to fill the screen, you just need to zoom in more.
      2-The tutorial assumes that all tiles are 256*256 images, the tiles you downloaded might be smaller, in the code try getting a single tile as a bitmap and check its dimensions, if they are actually smaller you have to change:

      • MapView.java line 83
      • MapView.java line 146

      If the database you have isn’t large you can send it to me and I’ll see how to deal with the issue.

  6. 15/06/2012 at 10:52 am

    Thank you so much.I will send the small size database.I use the android emulator version 2.3.4.(screen size 4, storage 2GB)
    what is your email address?
    I’m very sorry for your time consuming.

    • Fuchs
      15/06/2012 at 1:24 pm

      Okay I tried the map you sent me and it works fine, the tiles are correctly sized, the space you find is preserved for tiles that are not available in the database, in other words:tiles you didn’t download.
      There’s nothing you can really do, you can’t display what’s not there!
      As I said before zooming in more will make the map fill the screen (If you download the tiles at higher zoom levels of course).

      Since you’re downloading the tiles for a city you can select more zoom levels.

  7. 16/06/2012 at 3:06 pm

    Hi Fuchs,

    Thank for remember,I have a idea.let , my downloaded database is high zoom level(1 to 15),I used this zoom level for my application within zoom level 5 to 15. when i start run the application,the zoom level must start from level 5. I don’t want to use zoom level 1 to 4.
    Can I ok ?Please give me advice.
    And then ,I need to display the information onto mapview.
    How to create the popup window?Please give me a sample code.
    Thank you so much

    • Fuchs
      16/06/2012 at 5:51 pm

      The first one is easy:
      In TilesManager.java line:146

      public void setZoom(int zoom)
      {
      	zoom = (int) clamp(zoom, <strong>5</strong>, maxZoom);
      	updateVisibleRegion(location.x, location.y, zoom);
      }
      

      About the second one, you can display whatever you want on top of the map, as I said earlier
      https://ghoshehsoft.wordpress.com/2012/04/06/mapapp5-mapview-and-activity/#comment-351

      What kind of info do you want to display and what do you mean with the popup window?

  8. 17/06/2012 at 9:48 am

    Hi fuchs,
    Thank for remember.I want to display the location information from sqlitedb onto mapview .
    1 . I marks the point on the mapview,and then marks the regions(polygon).
    2. That points and region stored in sqlitedb.
    3. I display the location information when i click the marked point.So ,when i click the marked point, the popup(baloons) box is appear on mapview with relevant infromation.

    thank you for your answer.If you have any idea for above mentions.please share to me.

    • 21/06/2012 at 9:45 am

      http://deckjockey.blogspot.com/2010/01/android-baloon-display-on-map.html?showComment=1340264193387#c1311029246885376769

      Hi Fuchs,I reference above link for Popup box,But i am facing the error,My error is

      MapView.LayoutParams(200,200,item.getPoint(),MapView.LayoutParams.BOTTOM_CENTER));

      my MapView can not use LayoutParams.

      How do I work?
      Please help me

      • Fuchs
        21/06/2012 at 10:45 pm

        It will be easier for me if you send me some code.

        • 22/06/2012 at 4:28 pm
          public class BaloonLayout extends LinearLayout {
          
              .......
          
              @Override
              protected void dispatchDraw(Canvas canvas) {       
                  Paint panelPaint  = new Paint();
                  panelPaint.setARGB(0, 0, 0, 0);
                         
                  RectF panelRect = new RectF();
                  panelRect.set(0,0, getMeasuredWidth(), getMeasuredHeight());
                  canvas.drawRoundRect(panelRect, 5, 5, panelPaint);
                 
                  RectF baloonRect = new RectF();
                  baloonRect.set(0,0, getMeasuredWidth(), 2*(getMeasuredHeight()/3));
                  panelPaint.setARGB(230, 255, 255, 255);       
                  canvas.drawRoundRect(baloonRect, 10, 10, panelPaint);
                 
                  Path baloonTip = new Path();
                  baloonTip.moveTo(5*(getMeasuredWidth()/8), 2*(getMeasuredHeight()/3));
                  baloonTip.lineTo(getMeasuredWidth()/2, getMeasuredHeight());
                  baloonTip.lineTo(3*(getMeasuredWidth()/4), 2*(getMeasuredHeight()/3));
                 
                  canvas.drawPath(baloonTip, panelPaint);
                         
                  super.dispatchDraw(canvas);
              }
          }
          

          – Define a layout xml for this layout and add views that will be inside this layout like:
          – In your map activity (where you will have your mapview) create an instance of layout we defined:

                  LayoutInflater layoutInflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                  noteBaloon = (BaloonLayout) layoutInflater.inflate(R.layout.baloon, null);
                  RelativeLayout.LayoutParams layoutParams   = new RelativeLayout.LayoutParams(200,100);
                  layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
                  layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
                  noteBaloon.setLayoutParams(layoutParams);   
          

          – Now whenever you need to display the baloon, just call mapview’s addView method as:

          mapView.removeView(noteBaloon);
          noteBaloon.setVisibility(View.VISIBLE);
                               ((TextView)noteBaloon.findViewById(R.id.note_text)).setText(msg.getData().getString(HANDLER_MESSAGE_AUTHOR)+"\n"+msg.getData().getString(HANDLER_MESSAGE_NOTE));
          mapController.animateTo(noteOverlay.getTapPoint());
          mapView.addView(noteBaloon, new MapView.LayoutParams(200,200,noteOverlay.getTapPoint(),MapView.LayoutParams.BOTTOM_CENTER));
          mapView.setEnabled(false);
          
  9. 17/06/2012 at 5:18 pm

    Hi fuchs,
    how to get the latitute and longitude when i click onto the mapview wherever.?
    Please help me.

  10. 22/06/2012 at 4:46 pm

    Hi,fuchs
    Thank for your reply.Now i get the lat & long when i click on the mapView because of your
    GuideLine.
    I need to know the next one.

    How to add the hightlight area?
    When I click the highlight area in mapview,the hightlight area is returnd with information box.

    Example.
    In the World map,when I click the Africa area any where in mapView.The message return
    “This is Africa”.
    But,I don’t want to use internet connection.

    If you have any idea,Please Help me.
    Thankyou.

    • Fuchs
      25/06/2012 at 3:56 pm

      I’m not sure how this will work without internet connection.
      You can pre-download location names (through reverse geocoding) and store them in a database along with the longitude latitude and provide this database with your app, when you want the name of a specific location you pick the nearest match from the database.
      One downside of this method is the accuracy issue, downloading more locations names will increase the accuracy and database size.

  11. 10/07/2012 at 5:43 am

    Hi Fuchs,
    Can I convert the Would.sqlitedb to spatial database?
    How to convert this is?
    Please help me?

    Thank you for your answer.

  12. 12/07/2012 at 6:07 pm

    Hi Fuchs,
    Thank you for your answer.
    how do i use this spatial data for whole world to display on the android emulator.
    I want to use this spatial data instead of World.sqlitedb.
    How should I do?
    Please help me.

    • Fuchs
      12/07/2012 at 9:33 pm

      I’m sorry but I know nothing about this particular subject, You’ll have to examine how the spatial data is organized and if needed convert them to an Sqlite database to use on the mobile.

  13. 12/08/2012 at 6:56 am

    Hi Fuchs,
    I want to add webserver on your project.
    How do I add the webserver?
    Can I store the sqlite database in android webserver?

    • Fuchs
      13/08/2012 at 4:12 am

      Okay I started writing a short tutorial about that, it should be ready soon I guess 😀

  14. 13/08/2012 at 11:23 am

    Thank Fuchs,
    I am waiting for you.you can sent to my email when your program is finished.
    This is very improtant for me.
    hlaingwintunn@gmail.com

    • Fuchs
      13/08/2012 at 12:35 pm

      Okay I’m cooking the tutorial now 🙂

    • Fuchs
      16/08/2012 at 9:50 pm

      Okay the tutorial is ready with full implementation :).
      MapApp6: Web support

  15. 17/08/2012 at 4:49 pm

    Hi fuchs,
    Thank you for your tutorial.
    You are great man!

    • Fuchs
      17/08/2012 at 8:33 pm

      you’re welcome my friend :).

  16. chandanamal
    03/10/2012 at 2:40 pm

    can you tell me how to set map view to layout xml file and coding it in activity file. thanks

    • Fuchs
      03/10/2012 at 10:58 pm

      I haven’t tried that yet, you can check the second answer here on stacktoverflow, it contains explanation and example.

  17. bushra
    21/02/2013 at 1:57 pm

    hi, i tried downloading the map using mobile atlas creator version 1.9.11, and selected RMaps Sqlite format, then i tried to use it in your application , but it opened a gray page! the map is not showing…knowing that i named the file world.sqlitedb and replaced it with your map…need to know what i am doing wrong and why isn’t it working…thx.

    • Fuchs
      21/02/2013 at 4:29 pm

      Hi, I downloaded the 1.9.11 version and tried it and it worked, just make sure to select the lower zoom levels in addition to the zoom levels you need.
      Let’s say you want to download zoom level 5 then you should also include levels 0,1,2,3 and 4.
      The reason for this is that you need something to guide you while zooming-in in the app till you reach the desired zoom level, otherwise you might get lost in the gray background.
      I Hope this helps.

      • bushra
        22/02/2013 at 8:46 am

        hi,
        thxs this worked perfectly after i added zoom levels 0 to 4,. but what if i need to start zooming from a higher level ..like 7 or 8 how can i do it? and also, i need to use some detailed maps showing points like restaurants or gas stations, but unfortunately, there are some areas blocked from the map…how can i solve this? if it is not possible, can i use my own map, maybe convert it to rmaps sqlite format, also i don’t know how to do that!
        thank you very much, this tutorial is very helpful, you saved me 🙂

        • Fuchs
          22/02/2013 at 7:54 pm

          Hi, the app saves the last zoom level the user used, so the next app launch will have the same zoom level when the app was closed.
          If you want the app to always start at a specific zoom level you can simply change the line that restores the zoom level from the preferences, just go to MapAppActivity.restoreMapViewSettings method and you’ll see the line:

          mapView.setZoom(zoom);
          

          change it to the zoom level you want like this:

          mapView.setZoom(7);
          

          This way the app first run will start with gps coordinates equal to (0,0) and zoom level 7, you might also get lost here in the gray background since you might not have the tiles around (0,0) at zoom level 7, that’s why I don’t recommend messing with the starting zoom level since it’s better to let the user decide what zoom level he wants.

          About the blocked areas this might happen when downloading OpenStreetMaps tiles quickly as this process will exhaust their servers, a solution is to download them slowly setting a speed limit on Mobile Atlas Creator, another (and better) solution is to download tiles only when needed which is explained in the tutorial part 6 Web Support.

          • bushra
            25/02/2013 at 9:42 am

            thank you very much.

  18. Nguyen Son
    24/02/2013 at 7:17 pm

    Hi fuchs,
    Thank you for your tutorial.
    I have tried to add a buttion (or image buttion) on the layout contain mapview. It is invisible. why?

    • Fuchs
      25/02/2013 at 8:20 pm

      Hi 🙂 did you add the button before or after the map view?
      I’m writing a tutorial right now about this, in the mean time you can check this comment here (#95).

      EDIT: The tutorial is finished, you can read it here : MapApp7 : Creating MapView in XML

  19. bushra
    04/03/2013 at 1:33 pm

    hi, thank you for your help.. i need to plot many points on the map, not only one point, and draw a line between those points, how can i do this?

    • Fuchs
      05/03/2013 at 3:53 am

      That can be done easily, it just depends on the points you want to draw, are they gps coordinates? if so then you need to convert each point to map pixel coordinates using
      TilesManager.lonLatToPixelXY
      and then subtract the offset (which is calculated in the mapView).

      If you want to take a look at my implementation you can check the post I just published:
      Fuchs Maps

      You can take a look how things can be done and then implement it your own way.
      If you need some explanation about the code tell me and I’ll give you more details.

  20. Nguyen Hang Nga
    18/06/2013 at 12:01 pm

    Hi Fuchs,

    I just noticed your app enable the GPS function. However when I turned on GPS in my nexus 7, there is nothing appeared? Is it supposed to have my location displayed on map?
    Moreover, when I enabled track option, then saved it but the app announced that that record is empty.

    Could u please tell me why or just guide me how to enable all requirements to have the options done successfully.

    Best regards,

  21. Nguyen Hang Nga
    18/06/2013 at 12:05 pm

    Oh my bad, my previous comment is for Fuchs Maps example.
    Sorry for putting in wrong page

    • Fuchs
      19/06/2013 at 7:47 pm

      No need to apologize 🙂

  22. Vinícius Tostes
    27/06/2013 at 3:15 pm

    I need some help with something I have no idea how to do.
    Now that I can make this base of the map, is there anyway to add a layer above the current layout? Like an .KML file or anothers made by tiles too?
    I’ll be glad for any help!

    • Fuchs
      27/06/2013 at 7:54 pm

      I guess that can be done.

      First Approach : Double things
      1- Add another TilesProvider to the MapView class. It could be just another TilesProvider with a different database as a source for tiles.
      2- In MapView.fetchTiles() you have to fetch the tiles for the second layer (using same tileManager calculations), something like this:
      otherTileProvider.fetchTiles(visibleRegion, tileManager.getZoom());
      3- Create a second drawTiles in MapView that renders the tiles from the newly added TilesProvider.
      4- Add call to the newly defined drawTiles just after the old one.

      Second Approach : Use a Single Database with More Data
      1- You can use a the same TilesProvider with a tiles database that contains (in addition to current columns) a column that specifies the layer which the tile belongs to. The query inside the TilesProvider doesn’t have to be changed since it only cares about x,y and z.
      2- In MapView.drawTiles you do two iterations on the tiles you have, in the first iteration you draw the tile if it belongs to the base layer, in the second iteration you draw the tile if it belongs to the second layer.

      I guess the second approach is better but it requires you to have control over the database contents, the first approach allows you to combine two different databases.

      If you’ve been reading the tutorials you should be able to make sense of the above.

      The above approaches still use RMaps sqlite database format, I never used KML but I guess the concepts are similar,
      Let me know if you need any help.

  23. Faisal
    08/01/2014 at 7:43 pm

    That’s great mate..From last 2-3 days i was looking for something like this until i reached here and the way you explained things is great. I’m trying to develop this kind of app and i assume your tutorials are gonna help me a lot. Hats off 🙂 I just downloaded apk and placed database in sd card under folder name you have given. When i try to open app it gives message that Unfortunately, app stopped working. Any ideas? I’m using it on Sony Xperia Z. Thanks and once again please accept my gratitude 🙂

    • Fuchs
      10/01/2014 at 8:19 pm

      Hi Faisal and thanks for your interest. I’m sorry it took me a while to reply.
      In order to know what exactly happened you need to check the LogCat in Eclipse\Android studio :).

  24. 25/06/2014 at 5:08 am

    Hi Fuchs! Thanks for the tutorial….! It really helped me a lot for making the
    offline map under android 2.3 here. 😀

    My question is if let say….
    My layout is like this

    Would it be possible if you guive me
    how to put the map into the Red Box area?
    if let’s say the red area is another small frameLayout / ImageView or something….

    • Fuchs
      25/06/2014 at 6:56 pm

      Hi :).
      You simply need to:
      1- Create the map view
      2- Get a reference to the FrameLayout in red
      3- Call myFrameLayout.addView(mapView);

      If you need more details please let me know.

      • 23/08/2014 at 9:54 am

        hi fuchs, i’m back. err,, i found a little bit confusing over here. Are you checking out your blogs lately ? I post some comments there that may need your help (tips).

        • Fuchs
          26/08/2014 at 7:05 pm

          Hi, is the problem about embedding the MapView in the red frame or is it about the map tiles source?

  25. Phong Ngo
    25/08/2014 at 12:58 pm

    Hi there, I tried to download and install the app on my phone but it didnt work. A message shows the app cannt run because of error at opening app. I dont know what is going on and how to fix it. Could you help me please because I write an app and it uses offline map in emergency situation ?
    Many thanks

  26. 08/05/2016 at 1:39 pm

    Dear FUCHS, SALAM
    my phone has ultra HD resolution and phone is showing maps with very small text size of addresses on MAPS … how can I increase that text size on maps ? 😦

    • Fuchs
      10/05/2016 at 12:44 pm

      Salam Farhan.
      Unfortunately back then I used pixels to specify dimensions. To solve the issue you need to use dp units.

  27. Crazy4Tech
    26/08/2016 at 3:52 pm

    Hi Hisham,
    First of thank you for the great efforts in putting together these series of posts.
    I learnt a lot not only about the writing the app but also concepts.
    It is really appreciated.
    Now I am using newercurrent version of android SDK and seeing errors in opening “world.sqlitedb” file. Looks like there is an issue with sqllite itself.
    I can post/send the exact error later. Have you built the mapapp with later version of android ?

    • Fuchs
      27/08/2016 at 2:28 am

      Glad I could help 🙂
      I really wish I have the time and patience to re-write this tutorial series since a lot of the code could have been written better.
      What errors are you receiving?

  28. Crazy4Tech
    28/08/2016 at 11:14 pm

    Hi Hisham,
    thanks for the reply. I understand you are busy. I did some debugging my self. I am seeing error with World.sqlitedb itself. The error is 14.
    Could you please email me the World.sqlitedb file to me directly?

    Thanks

  1. No trackbacks yet.

Leave a Reply to hlaingwintun Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: