Skip to content

Instantly share code, notes, and snippets.

@typebrook
Last active January 12, 2024 08:46
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save typebrook/7d25be326f0e9afd58e0bbc333d2a175 to your computer and use it in GitHub Desktop.
Save typebrook/7d25be326f0e9afd58e0bbc333d2a175 to your computer and use it in GitHub Desktop.
How to make MBTiles valid on Mapbox Android SDK #mbtiles #android #mapbox

MBTilesSource.kt + MbtilesServer.kt

What it does?

It enables a MapboxMap (from Mapbox Android SDK) to use MBTiles as Source.

Inspired from similar Swift approach in iOS SDK, made by @namannik.

How it works?

In you APP, when a MBTilesSource instance activates, it starts a localhost MBTilesServer and works like a common Mapbox Raster/Vector Source.

MBTilesServer is a Singleton. It can hosts multiple MBTilesSource. The URL for each MBTilesSource is http://localhost:8888/[Source ID]/{z}/{x}/{y}.[jpg|png|pbf|mvt]

Since MBTilesSource only works as common Mapbox Raster/Vector Source, you always need to add layer or set style for MapboxMap by yourself. Mapbox Style is a bit more complex. If your MBTiles contains vector tile (.mvt), make sure your specify correct value for source-layer. See spec here.

Also, you may check an example by MapTiler, an OSM vector tiles provider.

Installation

  • Add MBTilesSource.kt and MBTilesServer.kt (from this gist) into your Android project.

Usage

Prepare network configuration

To make sure MBTilesServer works without network connection above Android 9 (API level 28), you have to allow HTTP traffic to localhost.

For more information, see Android Developers.

  1. Add network configuration into AndroidManifest.xml
    android:networkSecurityConfig="@xml/network_security_config" >
  2. A resource file named in AndroidManifest.xml
    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <domain-config cleartextTrafficPermitted="true">
            <domain includeSubdomains="false">localhost</domain>
        </domain-config>
    </network-security-config>

Create a MBTilesSource to read your MBTiles file

// Connect to localhost even when device is not connected to internet
Mapbox.setConnected(true)

// Create MBTilesSource
val path = "/path/to/source.mbtiles" // file path
val sourceId = "source1" // used as Mapbox Source ID
val mbSource = try {
    MBTilesSource(path, sourceId).apply { activate() }
} catch (e: MBTilesSourceException.CouldNotReadFileException){
    // Deal with error if fail to read MBTiles
}

Now, a XYZ Tile Service is working on http://localhost:8888/source1/{z}/{x}/{y}.[mvt|pbf|jpg|png].

The file extension of each tile is judged by the format in MBTiles. (Defined in table metadata, only mvt, pbf, jpg, png are allowed)

Also, if your MBTiles is at asset folder, you may use companion object in MBTilesSource to copy it into internal storage, and get the path directly.

val path = MBTilesSource.readAsset(context, "FILENAME.mbtiles")

Remove a source from MBTilesServer

 mbSource.deactivate()

Work with Mapbox Style

// In callback of Style.OnStyleLoaded
style.addSource(mbSource.instance)

// If mbSource contains raster tiles
val rasterLayer = RasterLayer("raster_layer_id", mbSource.id)
style.addLayer(rasterLayer)

// If mbSource contains vector tiles
val vectorLayer = LineLayer("vector_layer_id", mbSource.id)
style.addLayer(vectorLayer)

How it looks like?

I created this for my app, Five More Minutes. This demo use vector MBTiles and style from OpenMapTiles.

Alternatives

  • If you want to create an APP with map which really reads MBTiles directly, consider use Tangram SDK instead of Mapbox SDK. It natively support use a MBTiles as source offline, see Documentation here.

    Do not challenge why Mapbox doesn't support a format they created :D

  • Or, use Maplibre-gl-native, a mapbox-gl-native fork which embraces open source!

  • If it won't bother you to store tiles with folders, you can use asset:// and file:// protocol with source. See related thread in StackOverflow here.

CHANGELOG

  • 2021-01-26
    • Add Alternatives section
  • 2020-11-25
    • Add network configuration for newer Android version, thanks @Cacaonut !
    • Remove Anko from dependencies
    • Code refactor
    • Exmaple refactor for latest Mapbox API
  • 2020-05-26
package com.example.sample.offline
import android.util.Log
import java.io.BufferedReader
import java.io.ByteArrayOutputStream
import java.io.FileNotFoundException
import java.io.PrintStream
import java.net.ServerSocket
import java.net.Socket
import kotlin.math.pow
/*
* Localhost tile server with MBTilesSource
*/
object MBTilesServer : Runnable {
const val port = 8888
private val serverSocket: ServerSocket = ServerSocket(port)
var isRunning = false
val sources: MutableMap<String, MBTilesSource> = mutableMapOf()
fun start() {
isRunning = true
Thread(this).start()
}
fun stop() {
isRunning = false
serverSocket.close()
}
override fun run() {
try {
while (isRunning) {
serverSocket.accept().use { socket ->
Log.d(javaClass.simpleName, "Handling request")
handle(socket)
Log.d(javaClass.simpleName, "Request handled")
}
}
} catch (e: Exception) {
Log.d(
javaClass.simpleName,
e.localizedMessage ?: "Exception while running MBTilesServer"
)
} finally {
Log.d(javaClass.simpleName, "request handled")
}
}
@Throws
private fun handle(socket: Socket) {
val reader: BufferedReader = socket.getInputStream().reader().buffered()
// Output stream that we send the response to
val output = PrintStream(socket.getOutputStream())
try {
var route: String? = null
// Read HTTP headers and parse out the route.
do {
val line = reader.readLine() ?: ""
if (line.startsWith("GET")) {
// the format for route should be {source}/{z}/{x}/{y}
route = line.substringAfter("GET /").substringBefore(".")
break
}
} while (line.isNotEmpty())
// the source which this request target to
val source = sources[route?.substringBefore("/")] ?: return
// Prepare the content to send.
if (null == route) {
writeServerError(output)
return
}
val bytes = loadContent(source, route) ?: run {
writeServerError(output)
return
}
// Send out the content.
with(output) {
println("HTTP/1.0 200 OK")
println("Content-Type: " + detectMimeType(source.format))
println("Content-Length: " + bytes.size)
if (source.isVector) println("Content-Encoding: gzip")
println()
write(bytes)
flush()
}
} finally {
reader.close()
output.close()
}
}
@Throws
private fun loadContent(source: MBTilesSource, route: String): ByteArray? = try {
val (z, x, y) = route.split("/").subList(1, 4).map { it.toInt() }
source.getTile(z, x, (2.0.pow(z)).toInt() - 1 - y)
} catch (e: FileNotFoundException) {
e.printStackTrace()
null
}
private fun writeServerError(output: PrintStream) {
output.println("HTTP/1.0 500 Internal Server Error")
output.flush()
}
private fun detectMimeType(format: String): String? = when (format) {
"jpg" -> "image/jpeg"
"png" -> "image/png"
"mvt" -> "application/x-protobuf"
"pbf" -> "application/x-protobuf"
else -> "application/octet-stream"
}
}
package com.example.sample.offline
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import com.mapbox.mapboxsdk.style.sources.RasterSource
import com.mapbox.mapboxsdk.style.sources.Source
import com.mapbox.mapboxsdk.style.sources.TileSet
import com.mapbox.mapboxsdk.style.sources.VectorSource
import java.io.File
import java.io.FileOutputStream
import kotlin.properties.Delegates
/*
* Mapbox Source backend by localhost tile server
*/
sealed class MBTilesSourceException : Exception() {
class CouldNotReadFileException : MBTilesSourceException()
class UnsupportedFormatException : MBTilesSourceException()
}
class MBTilesSource(filePath: String, sourceId: String? = null) {
val id = sourceId ?: filePath.substringAfterLast("/").substringBefore(".")
val url get() = "http://localhost:${MBTilesServer.port}/$id/{z}/{x}/{y}.$format"
private val db: SQLiteDatabase = try {
SQLiteDatabase.openOrCreateDatabase(filePath, null)
} catch (e: RuntimeException) {
throw MBTilesSourceException.CouldNotReadFileException()
}
val instance: Source by lazy {
if (isVector) VectorSource(id, TileSet(null, url))
else RasterSource(id, TileSet(null, url))
}
var isVector by Delegates.notNull<Boolean>()
lateinit var format: String
// var tileSize: Int? = null
// var layersJson: String? = ""
// var attributions: String? = ""
// var minZoom: Float? = null
// var maxZoom: Float? = null
// var bounds: LatLngBounds? = null
init {
try {
format = db.query(
"metadata", null, "name = ?",
arrayOf("format"), null, null, null
).use { cursor ->
cursor.moveToFirst()
val index = cursor.getColumnIndex("value")
cursor.getString(index)
}
isVector = when (format) {
in validVectorFormats -> true
in validRasterFormats -> false
else -> throw MBTilesSourceException.UnsupportedFormatException()
}
} catch (error: MBTilesSourceException) {
print(error.localizedMessage)
}
}
fun getTile(z: Int, x: Int, y: Int): ByteArray? {
return db.query(
"tiles", null, "zoom_level = ? AND tile_column = ? AND tile_row = ?",
arrayOf("$z", "$x", "$y"), null, null, null
).use { cursor ->
if (cursor.count == 0) return null
cursor.moveToFirst()
val index = cursor.getColumnIndex("tile_data")
cursor.getBlob(index)
}
}
fun activate() = with(MBTilesServer) {
sources[id] = this@MBTilesSource
if (!isRunning) start()
}
fun deactivate() = with(MBTilesServer) {
sources.remove(id)
if (isRunning && sources.isEmpty()) stop()
}
companion object {
val validRasterFormats = listOf("jpg", "png")
val validVectorFormats = listOf("pbf", "mvt")
fun readAsset(context: Context, asset: String): String =
context.assets.open(asset).use { inputStream ->
val path = context.getDatabasePath(asset).path
val outputFile = File(path)
FileOutputStream(outputFile).use { outputStream ->
inputStream.copyTo(outputStream)
outputStream.flush()
}
return path
}
}
}
@typebrook
Copy link
Author

typebrook commented Aug 2, 2020

@mortezaomidi
Did you check the comment from @Cacaonut?

@mortezaomidi
Copy link

mortezaomidi commented Aug 3, 2020

@mortezaomidi
Did you check the comment from @Cacaonut?

Yes. but the mentioned problem by @Cacaonut occurs to me when using the kujaku/mbtiole package . I create a repository here and I tested it. I would be thankful if someone could help me to display mbtiles from external storage (Specifically a Java solution).
I could not convert these two files (MBTilesSource.kt + MbtilesServer.kt) to the sample java app.

@typebrook
Copy link
Author

typebrook commented Aug 3, 2020

I would be thankful if someone could help me to display mbtiles from external storage (Specifically a Java solution).
I could not convert these two files (MBTilesSource.kt + MbtilesServer.kt) to the sample java app.

@mortezaomidi
Nah, I won't make another redundant files for Java version. Use Kotlin code doesn't mean you need to refactor your whole project.
Thanks to JetBrains, we can call Kotlin module without any issue in Java file. All you need to do is just add plugin for Kotlin, which has fully support by Google, and just add MBTilesSource.kt + MbtilesServer.kt.

I made a PR for things mentioned above at mortezaomidi/mbtiles#1. Since I don't have your sample mbtiles, you still need some efforts to make it works.

Anyway, your case reminds me that Mapbox already changed API. So instead of MapboxMap, Style is responsible for addSource() and addLayer() now. I'll update README later. Have a good day!

@mortezaomidi
Copy link

Finally I was able to display the ZXY tiles according to the MapBox "file://" protocol. Thanks to @typebrook and @githengi. I describ my problem here in detail.

The first issue that caused the problem was android permission. Considerations for display ZXY tiles by MapBox SDK:
First we define related permission in the AndroidManifest.xml:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
It should be noted that you may need to check required permission before adding the layer. For more information on that refer to the Request App Permissions. Then use the following code:

               `RasterSource rasterSource = new RasterSource("testTileSource", 
                  new TileSet("tileset",
                        "file://" + Environment.getExternalStorageDirectory() + "/tiles/{z}/{x}/{y}.png")
                );
                mapboxMap.getStyle().addSource(rasterSource);
                RasterLayer rasterLayer = new RasterLayer("testTileLayer",
                        "testTileSource");
                mapboxMap.getStyle().addLayer(rasterLayer);`

I used the Global Mapper software to produce "png" tiles. You should download ZXY tiles sample that i used .

Although the problem of rendering ZXY tiles from external storage was solved for me, it would be great if a compressed file (Like .mbtiles format) could be used in this protocol.

In this regard, MBTilesServer.kt developed by @typebrook and another solution for reading external mbtiles developed by @githengi. For more detail in the @githengi solution see kujaku/mbtiole package.

I tried to import kujaku/mbtiole package in my project and use it. It seems that Tile Server works properly, but the requests are made by Android policy canceled. Myy error log display:
cleartext communication to localhost not permitted by network security policy

I googled the error log and add "android:usesCleartextTraffic="true"" to my project but I failed. I dot have any idea, but it may be because of the versions of Mapbox or Android SDK(i used updated versions).

I think I talked a lot! So if we can create a simple app that display a 'Mbtiles file', it will help many people.

My knowledge is not advanced in Android programming but I will do my best to help.

I created a small project to help other people for displaying offline zxy tiles from external storage in mapbox SDK. In this project, I used PermissionsDispatcher to handle runtime permissions. I hope this can solve someone's problem.

1
2
3
4

@typebrook
Copy link
Author

typebrook commented Aug 4, 2020

@mortezaomidi

I created a small project to help other people for displaying offline zxy tiles from external storage in mapbox SDK. In this project, I used PermissionsDispatcher to handle runtime permissions. I hope this can solve someone's problem.

That's great! So I guess you already solve your issue in mapbox-gl-native-android?

Does the mbtiles package in onaio/kujaku works in your case? Before your post, I didn't know there is such a package for MBTiles in Mapbox SDK.

@mortezaomidi
Copy link

mortezaomidi commented Aug 4, 2020

I would be thankful if someone could help me to display mbtiles from external storage (Specifically a Java solution).
I could not convert these two files (MBTilesSource.kt + MbtilesServer.kt) to the sample java app.

@mortezaomidi
Nah, I won't make another redundant files for Java version. Use Kotlin code doesn't mean you need to refactor your whole project.
Thanks to JetBrains, we can call Kotlin module without any issue in Java file. All you need to do is just add plugin for Kotlin, which has fully support by Google, and just add MBTilesSource.kt + MbtilesServer.kt.

I made a PR for things mentioned above at mortezaomidi/mbtiles#1. Since I don't have your sample mbtiles, you still need some efforts to make it works.

Anyway, your case reminds me that Mapbox already changed API. So instead of MapboxMap, Style is responsible for addSource() and addLayer() now. I'll update README later. Have a good day!

@typebrook
Thanks for your PR. I was able to successfully display a raster PNG.

for me this line of code:

private val db: SQLiteDatabase = try { SQLiteDatabase.openOrCreateDatabase(filePath, null) } catch (e: RuntimeException) { throw MBTilesSourceError.CouldNotReadFileError() }

raised error and i chanced it to :

private val db: SQLiteDatabase = try { SQLiteDatabase.openDatabase(filePath, null, SQLiteDatabase.OPEN_READONLY) } catch (e: RuntimeException) { throw MBTilesSourceError.CouldNotReadFileError() }

Therefore, I confirm that by using the MBTilesSource.kt we can render mbtiles in newest MapBox SDK (mapbox-android-sdk:9.3.0).

1

New changes have been made to the mortezaomidi/mbtiles repo and you can see it here

Regarding vector tiles format, I think the project works well, only the definition of tile set needs to be defined. I will try to examine this issue as well.

@mortezaomidi
Copy link

@mortezaomidi

I created a small project to help other people for displaying offline zxy tiles from external storage in mapbox SDK. In this project, I used PermissionsDispatcher to handle runtime permissions. I hope this can solve someone's problem.

That's great! So I guess you already solve your issue in mapbox-gl-native-android?

Does the mbtiles package in onaio/kujaku works in your case? Before your post, I don't know there is such a package for MBTiles in Mapbox SDK.

Thanks for pointing me to the issue. I closed mapbox/mapbox-gl-native-android#342 (comment).

For me the mbtiles package in onaio/kujaku not works but i think the library works well with a little effort! Maybe @Cacaonut comment was the problem.

@mortezaomidi
Copy link

mortezaomidi commented Aug 4, 2020

A good news! This time I was able to show offline vector mbtiles by using the grate @typebrook MBTilesServer.kt.
download a sample OSM vector mbtile here and then follow this repository. For managing cartographic representation i use this. You can change it based on your preferences.

1

I consider it necessary to appreciate @typebrook this would not have been possible for me without her guidance..

@utya
Copy link

utya commented Aug 25, 2020

can i use sprite and glyphs localy?

@typebrook
Copy link
Author

@utya

can i use sprite and glyphs localy?

Yes, both of them supports asset:// and file:// protocol, see my comment at top.

@utya
Copy link

utya commented Aug 25, 2020

@typebrook
now i means instead of

"sprite": "https://rawgit.com/lukasmartinelli/osm-liberty/gh-pages/sprites/osm-liberty",
"glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf",

use somethitng like this

"sprite": "local_path",
"glyphs": "local_path"

i need offline solution,

@typebrook
Copy link
Author

@utya
That is exactly what I mentioned, check how Mapbox applys those protocol in the test APP:
https://github.com/mapbox/mapbox-gl-native/blob/5438af2/platform/android/MapboxGLAndroidSDKTestApp/src/main/assets/streets.json#L100

@utya
Copy link

utya commented Sep 2, 2020

maybe anybody generate mbtiles from geojson? i cant' make mbtiles from geojson and work with this mbtileserver. Works only mbtiles from openmaptiles

@typebrook
Copy link
Author

typebrook commented Sep 2, 2020

maybe anybody generate mbtiles from geojson?

@utya
You should check tippecanoe

@utya
Copy link

utya commented Sep 2, 2020

i genearated using this sourcs. but unfortunate. Could you send working sample created by tipprcanoe?

@typebrook
Copy link
Author

@utya
Well, generating MBTIiles is off topic in this gist. We should stop talking here.
If you have any trouble when creating MBTiles with tippecanoe, you'd better check trouble-shooting in mapbox website, or just create a new issue in tippecanoe repo. If you still have some issues unsolved but not related to this gist, a new stackoverflow post should be a good idea (Or just send me a mail)

@utya
Copy link

utya commented Sep 3, 2020

@typebrook
thanks and sorry

@TroyStopera
Copy link

TroyStopera commented Jan 17, 2021

I have a use case where users should be able to copy over their own mbtiles files and I'd like to use that as a source. I have it working for raster tiles, but vector (pbf specifically, but haven't tried mvt) are not rendering. I feel that it has something to do with layers. I've tried reading the json field from the metadata table, and using the layer IDs from there and creating LineLayer('id from json', source.id) and adding that to my Style. Still not working. Was hoping to get some tips or ideas. Thanks!

@typebrook
Copy link
Author

typebrook commented Jan 26, 2021

Hi ! @TroyStopera
Sorry for the late reply. In your reply, I think you are confused with "source layer"(The data in Vector Tiles) and "Mapbox layer" (The collection of features rendered in map).

You can define "Mapbox layer" id by yourself, it's not related to anything in metadata of a MBTiles. I think the json you mentioned is related about the list of "source layers", but it is not required in every MBTIles. And if a "Mapbox layer" use a specific "source layer", you need to use source-layer to do configuration, see Mapbox documentation

If you still have any problem, welcome to reply here.

@TroyStopera
Copy link

TroyStopera commented Jan 28, 2021

@typebrook thank you! I think I was confusing the two concepts. I'm still a bit confused on how to render these user-provided, vector-based mbtiles files though.

How can I find what source layers I need to add? Does the mbtiles file tell me? Also, how do I know what type of layer (line, circle, fill, etc...) to create and add to the style?

I'm understanding that I need to add these source layers to the style. It is really just figuring out what layers the mbtiles file has. I know that vector mbtiles files have a json metadata field that looks something like:

"vector_layers":[ { "maxzoom":14, "fields":{ "class":"String" }, "minzoom":0, "id":"water", "description":"" } ]

Which does tell me the layer id, but not what type of layer to use...
I've tried adding a LineLayer for each layer in the vector_layers array and setting the source-layer to match the id from the json but it doesn't seem to work.

@typebrook
Copy link
Author

typebrook commented Feb 2, 2021

How can I find what source layers I need to add? Does the mbtiles file tell me?

  1. Ask the provider of this MBTiles. For example, openmaptiles list their schema at https://openmaptiles.org/schema/
  2. According to MBTiles spec, the layers are described as json object only after 1.3 (as you mentioned

Also, how do I know what type of layer (line, circle, fill, etc...) to create and add to the style?

It depends on your usecase. For example, a source layer with geometry=LineString can be used with SymbolLayer or LineLayer.
If you are not familiar with these Mapbox styling spec, better to use Maputnik or Mapbox studio to do some practice/design.

@hungtrn75
Copy link

hungtrn75 commented Aug 16, 2021

Hi @typebrook. I'm using Mapbox version 10.0.0-rc.6, I can load mbtiles but I can add any Annotation or GeoJsonSource after load style uri from asset. How can I fix that issue. Thank you so much

@typebrook
Copy link
Author

typebrook commented Aug 16, 2021

Hi @hungtrn75

I can load mbtiles but I cannot add any Annotation or GeoJsonSource after load style uri from asset

Because you said you can load mbtiles, then I guess MBTilesSource works in your case.
To be clear, a style contains several sources as source of map data, and MBTilesSource is just one of them.

So if you cannot add any new source into current style, then I guess it's because you didn't call Mapbox APIs well.
But anyway, I think your issue is not related to this gist. Maybe make a new stackoverflow post would helps.

@hungtrn75
Copy link

hungtrn75 commented Aug 18, 2021

If I remove the code to load style json from asset, it works fine. I realized that OnStyleLoaded couldn't be called when passing styleUri as an asset so I thought it was a Mapbox issue. Thanks for quick response @typebrook

@alenigrelli
Copy link

alenigrelli commented Aug 19, 2021

Hello! I have a tiles server, its url is as follows http: // xxxxx / tiles / {x} / {y} / {z} .pbf what I need instead of taking, the files from the cell phone are from that url of my server ... is that possible in this implementation?

@typebrook
Copy link
Author

typebrook commented Aug 20, 2021

@alenigrelli
You have your own tile server? Then of course you don't need this gist.

If you are using Mapbox SDK or any other tools (Like MapLibre) which support Mapbox style spec on your mobile APP, just add endpoint of your tile server as one of Vector Source in style.

@ahmedmansour3548
Copy link

ahmedmansour3548 commented Feb 5, 2023

Hi, will this see an update with the Mapbox SDK V10 changes? Adding the MBTilesSource using addSource() like before:

style.addSource(mbSource.instance)

Is giving:

com.mapbox.maps.MapboxStyleException: Add source failed: source must have tiles

This is with a sample mbtiles file (countries.mbtiles from this). Will report back if I find a solution.

@typebrook
Copy link
Author

typebrook commented Feb 6, 2023

Woo~ It's been a long time I review this gist.

I guess the problem is here:
https://gist.github.com/typebrook/7d25be326f0e9afd58e0bbc333d2a175#file-mbtilessource-kt-L31-L34

Depends on the content of MBTiles, instance either return a RasterSource or VectorSource
Looks like from SDK V10, The argument for their constructor changes:

// Before V9
// https://docs.mapbox.com/android/maps/api/9.7.1/
fun VectorSource(id: String id, uri: Uri)

// V10
// https://docs.mapbox.com/android/maps/api/10.10.1/mapbox-maps-android/com.mapbox.maps.extension.style.sources.generated/-vector-source/
fun VectorSource(builder: VectorSource.Builder)

@ahmedmansour3548
Would you mind fix this by yourself and give me a PR?

Although I can directly change code by document. But to test the code, it took me a while to prepare Android development environment on my PC. (I don't code for Android recently)

PR could be done by the following steps:

  1. Fork this gist
  2. Make a new commit (Either on gist.github.com or push from local)
  3. Comment here with your gist URL (Or git remote URL), or mail me patch files from the following command:
    git format-patch 39fca96

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment