73

I would like to use the new TileProvider functionality of the latest Android Maps API (v2) to overlay some custom tiles on the GoogleMap. However as my users will not have internet a lot of the time, I want to keep the tiles stored in a zipfile/folder structure on the device. I will be generating my tiles using Maptiler with geotiffs. My questions are:

  1. What would be the best way to store the tiles on the device?
  2. How would I go about creating a TileProvider that returns local tiles?
nkorth
  • 1,684
  • 1
  • 12
  • 28
Gyroscope
  • 3,121
  • 4
  • 26
  • 33
  • In the same i would like to know 1 thing, is Google Map V2 provide a facility to download/caching the tile? (http://prntscr.com/3cyiqf) b'cos i'm confused in this case, means if they provide how to load/use tile using TileProvider Class than it should be something available for the Tile Caching/downloading. My actual requirement is i need to download/cache the map according to user's requirement. I have already checked OSMDROID lib but i want to use google map v2 only – Rajan Apr 25 '14 at 07:15
  • @Rajan I stumbled with the same issue. It seems like it possible to use tileProvide for caching. What you decided to use? – ar-g Oct 30 '14 at 11:07
  • @Gyroscope, From where did you get these tiles ? are they available to download ? – Heshan Sandeepa May 23 '16 at 17:39

3 Answers3

177
  1. You can put tiles into assets folder (if it is acceptable for the app size) or download them all on first start and put them into device storage (SD card).

  2. You can implement TileProvider like this:


public class CustomMapTileProvider implements TileProvider {
    private static final int TILE_WIDTH = 256;
    private static final int TILE_HEIGHT = 256;
    private static final int BUFFER_SIZE = 16 * 1024;

    private AssetManager mAssets;

    public CustomMapTileProvider(AssetManager assets) {
        mAssets = assets;
    }

    @Override
    public Tile getTile(int x, int y, int zoom) {
        byte[] image = readTileImage(x, y, zoom);
        return image == null ? null : new Tile(TILE_WIDTH, TILE_HEIGHT, image);
    }

    private byte[] readTileImage(int x, int y, int zoom) {
        InputStream in = null;
        ByteArrayOutputStream buffer = null;

        try {
            in = mAssets.open(getTileFilename(x, y, zoom));
            buffer = new ByteArrayOutputStream();

            int nRead;
            byte[] data = new byte[BUFFER_SIZE];

            while ((nRead = in.read(data, 0, BUFFER_SIZE)) != -1) {
                buffer.write(data, 0, nRead);
            }
            buffer.flush();

            return buffer.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        } catch (OutOfMemoryError e) {
            e.printStackTrace();
            return null;
        } finally {
            if (in != null) try { in.close(); } catch (Exception ignored) {}
            if (buffer != null) try { buffer.close(); } catch (Exception ignored) {}
        }
    }

    private String getTileFilename(int x, int y, int zoom) {
        return "map/" + zoom + '/' + x + '/' + y + ".png";
    }
}

And now you can use it with your GoogleMap instance:

private void setUpMap() {
    mMap.setMapType(GoogleMap.MAP_TYPE_NONE);

    mMap.addTileOverlay(new TileOverlayOptions().tileProvider(new CustomMapTileProvider(getResources().getAssets())));

    CameraUpdate upd = CameraUpdateFactory.newLatLngZoom(new LatLng(LAT, LON), ZOOM);
    mMap.moveCamera(upd);
}

In my case I also had a problem with y coordinate of tiles generated by MapTiler, but I managed it by adding this method into CustomMapTileProvider:

/**
 * Fixing tile's y index (reversing order)
 */
private int fixYCoordinate(int y, int zoom) {
    int size = 1 << zoom; // size = 2^zoom
    return size - 1 - y;
}

and callig it from getTile() method like this:

@Override
public Tile getTile(int x, int y, int zoom) {
    y = fixYCoordinate(y, zoom);
    ...
}

[Upd]

If you know exac area of your custom map, you should return NO_TILE for missing tiles from getTile(...) method.

This is how I did it:

private static final SparseArray<Rect> TILE_ZOOMS = new SparseArray<Rect>() {{
    put(8,  new Rect(135,  180,  135,  181 ));
    put(9,  new Rect(270,  361,  271,  363 ));
    put(10, new Rect(541,  723,  543,  726 ));
    put(11, new Rect(1082, 1447, 1086, 1452));
    put(12, new Rect(2165, 2894, 2172, 2905));
    put(13, new Rect(4330, 5789, 4345, 5810));
    put(14, new Rect(8661, 11578, 8691, 11621));
}};

@Override
public Tile getTile(int x, int y, int zoom) {
    y = fixYCoordinate(y, zoom);

    if (hasTile(x, y, zoom)) {
        byte[] image = readTileImage(x, y, zoom);
        return image == null ? null : new Tile(TILE_WIDTH, TILE_HEIGHT, image);
    } else {
        return NO_TILE;
    }
}

private boolean hasTile(int x, int y, int zoom) {
    Rect b = TILE_ZOOMS.get(zoom);
    return b == null ? false : (b.left <= x && x <= b.right && b.top <= y && y <= b.bottom);
}
Alex Vasilkov
  • 6,215
  • 4
  • 23
  • 16
  • 1
    Please note the documentation of the return value of getTile(int,int,int): the Tile to be used for this tile coordinate. If you do not wish to provide a tile for this tile coordinate, return NO_TILE. If the tile could not be found at this point in time, return null and further requests might be made with an exponential backoff. – JesperB Jun 24 '13 at 06:29
  • Thanks @JesperB, please see **[Upd]** secton of the answer – Alex Vasilkov Jun 25 '13 at 10:07
  • Thanks - I had a clunky version of this that I wrote when API v2 first came out and your example vastly improved my performance. FWIW, my tile set does not form a perfect rectangle, so I check for existence of the tile by checking whether the inputstream is null. – alice_silver_man Oct 01 '13 at 04:27
  • Alex Vasilkov : in ` CameraUpdate upd = CameraUpdateFactory.newLatLngZoom(new LatLng(LAT, LON), ZOOM);` Which LAT , LAN i have to pass? – Harshal Kalavadiya Jul 13 '15 at 06:06
  • does above code download tiles into asset folder on first online launch, or do we have to place any tiles file to the folder ? – Abdul Wahab Sep 17 '15 at 07:29
  • Is there any way to implement in iOS ? – ManiaChamp Dec 29 '15 at 12:24
  • 1
    may I ask how does one get to know the values you entered in your SparseArray? . What are these four values in Rect() ? – azmuhak Apr 21 '16 at 11:15
  • Alex, please from where i can get the tiles ? – Heshan Sandeepa May 23 '16 at 17:33
  • Guys you still neet your google api keys right? i mean we are still using our tiles.. – konzo Jun 03 '16 at 05:10
  • @AlexVasilkov , can we search place in the offline stored map using TileProvider. – Priya Aug 09 '17 at 07:21
  • @AlexVasilkov This doesn't compile for me, the signature for getTile is this: getTile(int i, int i1, int i2) as well. Any ideas why this isn't working? – Jay Jul 10 '18 at 03:28
  • @AlexVasilkov Hello it's working me but when I click on map my map disappear? any solutions? – Bipin Bharti Aug 18 '18 at 14:49
  • The above solution picks up assets from the resource bundle. What is to be done to download the maps at the first time the application is opened? – Arjun Issar Sep 26 '18 at 11:41
  • where to get map tiles? any working code of offline android map? – creativecoder May 17 '19 at 09:11
  • @AlexVasilkov I know exact area of your custom map but in topLeft and bottomRight coorditanes (latlong). How can i get TILE_ZOOMS from that? – Mayura Devani Mar 31 '22 at 12:54
8

The possibility of adding custom tileproviders in the new API (v2) is great, however you mention that your users are mostly offline. If a user is offline when first launching the application you cannot use the new API as it requires the user to be online (at least once to build a cache it seems) - otherwise it will only display a black screen.

EDIT 2/22-14: I recently came across the same issue again - having custom tiles for an app which had to work offline. Solved it by adding an invisible (w/h 0/0) mapview to an initial view where the client had to download some content. This seems to work, and allows me to use a mapview in offline mode later on.

erik_beus
  • 161
  • 1
  • 1
  • 7
  • 2
    Morty, from where do get this information? Is this the official intention of Google? This would make it impossible to use the gmap api v2 for displaying locally stored maptiles offline without internet connection. Tom – Tom Mar 06 '14 at 16:54
  • 1
    @Tom I'm afraid he's right. Yes it's really anoing. – Warpzit Apr 15 '14 at 09:55
  • In this situation you need to copy "ZoomTables.data" (pull it from emulator when you initialize map) to /data/data//files/ – Yuraj Feb 19 '16 at 12:46
  • can somebody elaborate about loading the custom map tiles first launch in airplane mode. – abhishek Nov 04 '16 at 18:10
  • I have the custom tiles, need to show them but getTile() does not get called on the first launch. I need to show custom tiles on first launch in airplane mode. – abhishek Dec 07 '16 at 18:09
0

This is how I implemented this in Kotlin:

class LocalTileProvider : TileProvider
{

override fun getTile(x: Int, y: Int, zoom: Int): Tile?
{
    // This is for my case only
    if (zoom > 11)
        return TileProvider.NO_TILE

    val path = "${getImagesFolder()}/tiles/$zoom/$x/$y/filled.png"
    val file = File(path)
    if (!file.exists())
        return TileProvider.NO_TILE
    return try {
        Tile(TILE_SIZE, TILE_SIZE, file.readBytes())
    }
    catch (e: Exception)
    {
        TileProvider.NO_TILE
    }
}

companion object {
    const val TILE_SIZE = 512
}

}
Sergey Stasishin
  • 291
  • 2
  • 10