Home > Android, Maps, Programming > MapApp4 : TilesProvider

MapApp4 : TilesProvider

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

Series outline:

______________________________________

So now we have a TilesManager to do all the math for us & provide us with tile indices, we still need something to give us the actual images of the tiles, this is what TilesProvider does each time our MapView asks for tiles images the TilesProvider will load the data from the Database then return them to the MapView.

Each time we need to get our tiles images we fetch the data from the map database and convert the data into a Bitmap that can be rendered in our MapView, querying the database is relatively a slow operation, to make it worse, building a Bitmap also needs some time, so we must implement some sort of optimization, I’m leaving the database part and focusing on the Bitmap creation.

Instead of recreating a new Bitmap each time we check to see if we already have this Bitmap in memory, we’ll see that later.

Since the tiles database already exists (We’re not creating it) you don’t need to know much about SQLite databases 🙂 just how to do a SELECT query :).

At the end of this post I’ll provide some info on how to make a TilesProvider that gets tiles from a web server :).

First of all we need a simple class called Tile, simply contains the index of the tile (x,y) and the image of this tile as a Bitmap

package com.mapapp.tileManagement;

import android.graphics.Bitmap;

public class Tile
{
	// Made public for simplicity
	public int x;
	public int y;
	public Bitmap img;

	public Tile(int x, int y, Bitmap img)
	{
		this.x = x;
		this.y = y;
		this.img = img;
	}
}

No Z index!, because when we change the zoom level of the map we will have to discard all the tiles we have and bring the new ones.
Now we can write the TilesProvider class, here are the fields and the constructor:

package com.mapapp.tileManagement;

import java.util.Hashtable;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.BitmapFactory;
import android.graphics.Rect;

public class TilesProvider
{
	// The database that holds the map
	protected SQLiteDatabase tilesDB;

	// Tiles will be stored here, the index\key will be in this format x:y
	protected Hashtable<String, Tile> tiles = new Hashtable<String, Tile>();

	public TilesProvider(String dbPath)
	{
		tilesDB = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.NO_LOCALIZED_COLLATORS | SQLiteDatabase.OPEN_READONLY);
	}

	// More code will be added here
}

One thing to note here is the Hashtable, I couldn’t find a Dictionary\Map class  in Java with 2 keys, so I’m using a hashtable and the key is a string combination of two keys, like this x:y, for example 3:1 means the tile with x=3 and y=1

SQLiteDatabase gives us the ability to perform queries on the database file that has the path we passed to its constructor.

The main function of this class is fetchTiles, it takes a rectangle specifying the index range of the tiles to get plus a zoom level.

For some reason the zoom levels stored in the database are subtracted from 17, so if I want the tile with xyz(1,3,2) I have to query for x=1 AND y=3 AND z=15, not a big deal 🙂 it just depends on how the database was stored.

Using a freeware program I checked the columns of the map database I have and it has this structure:

Tiles DB Columns

Tiles DB Columns



So now I know what to query for, and the method fetchTiles will be (pay attention to the comments, they are explaining everything):

	// Updates the tiles in the hashtable
	public void fetchTiles(Rect rect, int zoom)
	{
		// Perpare the query
		String query = "SELECT x,y,image FROM tiles WHERE x >= " + rect.left + " AND x <= " + rect.right + " AND y >= " + rect.top
				+ " AND y <=" + rect.bottom + " AND z == " + (17 - zoom); 		// query should be something like: 		// SELECT x,y,image FROM tiles WHERE x>=0 AND x<=4 AND y>=2 AND y<=6 AND z==6

		Cursor cursor;
		cursor = tilesDB.rawQuery(query, null);

		// Now cursor contains a table with these columns
		/*
		 * x(int)	y(int)	image(byte[])
		 */

		// Prepare an empty hash table to fill with the tiles we fetched
		Hashtable<String, Tile> temp = new Hashtable<String, Tile>();

		// Loop through all the rows(tiles) of the table returned by the query
		// MUST call moveToFirst
		if (cursor.moveToFirst())
		{
			do
			{
				// Getting the index of this tile
				int x = cursor.getInt(0);
				int y = cursor.getInt(1);

				// Try to get this tile from the hashtable we have
				Tile tile = tiles.get(x + ":" + y);

				// If This is a new tile, we didn't fetch it in the previous
				// fetchTiles call.
				if (tile == null)
				{
					// Get the binary image data from the third cursor column
					byte[] img = cursor.getBlob(2);

					// Create a bitmap (expensive operation)
					Bitmap tileBitmap = BitmapFactory.decodeByteArray(img, 0, img.length);

					// Create the new tile
					tile = new Tile(x, y, tileBitmap);
				}

				// The object "tile" should now be ready for rendering

				// Add the tile to the temp hashtable
				temp.put(x + ":" + y, tile);
			}
			while (cursor.moveToNext()); // Move to next tile in the query result

			// The hashtable "tiles" is now outdated,
			// so clear it and set it to the new hashtable temp.
			tiles.clear();
			tiles = temp;
		}
	}

after the tiles hashtable is updated, the MapView will be able to get those tiles using the method getTiles, adding two more methods, clear() which clears the tiles hashtable and close() which closes the database (these two methods are mainly used when closing the app) The full TilesProvider.java class should be like :

package com.mapapp.tileManagement;

import java.util.Hashtable;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;

public class TilesProvider
{
	// The database that holds the map
	protected SQLiteDatabase tilesDB;

	// Tiles will be stored here, the index\key will be in this format x:y
	protected Hashtable<String, Tile> tiles = new Hashtable<String, Tile>();

	public TilesProvider(String dbPath)
	{
		tilesDB = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.NO_LOCALIZED_COLLATORS | SQLiteDatabase.OPEN_READONLY);
	}

	// Updates the tiles in the hashtable
	public void fetchTiles(Rect rect, int zoom)
	{
		// Perpare the query
		String query = "SELECT x,y,image FROM tiles WHERE x >= " + rect.left + " AND x <= " + rect.right + " AND y >= " + rect.top
				+ " AND y <=" + rect.bottom + " AND z == " + (17 - zoom); 		// query should be something like: 		// SELECT x,y,image FROM tiles WHERE x>=0 AND x<=4 AND y>=2 AND y<=6 AND z==6

		Cursor cursor;
		cursor = tilesDB.rawQuery(query, null);

		// Now cursor contains a table with these columns
		/*
		 * x(int)	y(int)	image(byte[])
		 */

		// Prepare an empty hash table to fill with the tiles we fetched
		Hashtable<String, Tile> temp = new Hashtable<String, Tile>();

		// Loop through all the rows(tiles) of the table returned by the query
		// MUST call moveToFirst
		if (cursor.moveToFirst())
		{
			do
			{
				// Getting the index of this tile
				int x = cursor.getInt(0);
				int y = cursor.getInt(1);

				// Try to get this tile from the hashtable we have
				Tile tile = tiles.get(x + ":" + y);

				// If This is a new tile, we didn't fetch it in the previous
				// fetchTiles call.
				if (tile == null)
				{
					// Get the binary image data from the third cursor column
					byte[] img = cursor.getBlob(2);

					// Create a bitmap (expensive operation)
					Bitmap tileBitmap = BitmapFactory.decodeByteArray(img, 0, img.length);

					// Create the new tile
					tile = new Tile(x, y, tileBitmap);
				}

				// The object "tile" should now be ready for rendering

				// Add the tile to the temp hashtable
				temp.put(x + ":" + y, tile);
			}
			while (cursor.moveToNext()); // Move to next tile in the query result

			// The hashtable "tiles" is now outdated,
			// so clear it and set it to the new hashtable temp.
			tiles.clear();
			tiles = temp;
		}
	}

	// Gets the hashtable where the tiles are stored
	public Hashtable<String, Tile> getTiles()
	{
		return tiles;
	}

	public void close()
	{
		// If fetchTiles is used after closing it will not work, it will throw an exception
		tilesDB.close();
	}

	public void clear()
	{
		tiles.clear();
	}
}

Now the only piece of the puzzle we’re missing is the MapView and the activity that will hold this view 🙂

So in the next tutorial our map will be finally visible and  responsive :D.

______________________________________

Online map app?

Update 16/8/2012: Part 6 of the tutorial describes in detail how to support online maps, it also provides a full implementation.

That can be easily achieved I guess, since for most tiles servers you can get a tile using a url that contains the x,y,z index of the tile, something similar to this:

http://<server name>.com/x=2&y=4&z=5

this returns the image of the tile, which you can use to render in the MapView.

You can check this tutorial in the Android Training, it describes how to get data from a server asynchronously (without blocking the main UI thread).

Of course you don’t always ask the server for tiles each time you need them, you must implement some caching, if you want a tile you first look for it in your app memory, if not found you look for it in the database stored on the sd card, if not found you have no choice but to fetch it from the server then pass it to the other two cache levels (memory and database).

By having several cache levels you save the user some internet bandwidth ($ :)) and save the servers the trouble of processing your request.

Please note that some tiles servers prohibit storing their tiles on a storage device, some servers have limits on the number of tiles you can download before they can charge you for money :|, ALWAYS read the terms of service and any other agreements and usage policies the tiles servers have!, you don’t want trouble with the law :).

Categories: Android, Maps, Programming Tags: , ,
  1. DSC
    04/04/2012 at 12:26 am

    The app uses a query to search for tiles (eg. 1521<=x<= 1523, 985<=y<=987, z=13) and handles any rows found. Is a seperate webrequest required httpget for every possible x,y combination?
    I'm trying to switch to a webrequest in case the tile is not present in the database (and possibly add it to the database).

    • Fuchs
      04/04/2012 at 4:36 am

      I’m not actually sure but I think that depends on the tiles server, if the server provides tiles using direct links to png images for example I don’t think that’s possible.
      I think if you use threads to get tiles you won’t have speed problems.

    • jaggy
      16/04/2014 at 10:35 am

      hi i am trying to create the offline map.but its show fails to do it.can plase tell me step by step to create offline map using eclipse

  1. No trackbacks yet.

What do you think?

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: