Skip to content

Commit

Permalink
Add support of RMaps SQLite DB map files.
Browse files Browse the repository at this point in the history
  • Loading branch information
ComBatVision committed Sep 4, 2024
1 parent 2aea958 commit 32d46ad
Show file tree
Hide file tree
Showing 14 changed files with 255 additions and 16 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ repositories {
}
dependencies {
implementation 'earth.worldwind:worldwind:1.5.19'
implementation 'earth.worldwind:worldwind:1.5.22'
}
```

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ buildscript {

allprojects {
group = "earth.worldwind"
version = "1.5.21"
version = "1.5.22"

extra.apply {
set("minSdk", 21)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.j256.ormlite.dao.Dao
import earth.worldwind.render.image.ImageSource

actual fun buildImageSource(
tilesDao: Dao<MBTiles, Int>, readOnly: Boolean, contentKey: String, zoom: Int, column: Int, row: Int, imageFormat: String?
tilesDao: Dao<MBTiles, *>, readOnly: Boolean, contentKey: String, zoom: Int, column: Int, row: Int, imageFormat: String?
): ImageSource {
val format = when {
imageFormat.equals("image/jpeg", true) -> Bitmap.CompressFormat.JPEG
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import earth.worldwind.util.ResourcePostprocessor
import java.io.ByteArrayOutputStream

open class MBTilesBitmapFactory(
protected val tilesDao: Dao<MBTiles, Int>,
protected val tilesDao: Dao<MBTiles, *>,
protected val isReadOnly: Boolean,
protected val contentKey: String,
protected val zoom: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package earth.worldwind.layer.rmaps

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.j256.ormlite.dao.Dao
import earth.worldwind.layer.rmaps.RMapsTiles.Companion.S
import earth.worldwind.layer.rmaps.RMapsTiles.Companion.X
import earth.worldwind.layer.rmaps.RMapsTiles.Companion.Y
import earth.worldwind.layer.rmaps.RMapsTiles.Companion.Z
import earth.worldwind.render.image.ImageSource
import earth.worldwind.util.ResourcePostprocessor
import java.io.ByteArrayOutputStream

open class RMapsBitmapFactory(
protected val tilesDao: Dao<RMapsTiles, *>,
protected val isReadOnly: Boolean,
protected val contentKey: String,
protected val x: Int,
protected val y: Int,
protected val z: Int,
protected val format: Bitmap.CompressFormat,
protected val quality: Int = 100
): ImageSource.BitmapFactory, ResourcePostprocessor {
override suspend fun createBitmap(): Bitmap? {
// Attempt to read the ATAK tile data
val image = tilesDao.queryBuilder().where().eq(X, x).and().eq(Y, y).and().eq(Z, z).and().eq(S, 0)
.queryForFirst()?.image ?: return null

// Decode the tile data, either a PNG image or a JPEG image.
return BitmapFactory.decodeByteArray(image, 0, image.size)
}

override suspend fun <Resource> process(resource: Resource): Resource {
if (resource is Bitmap && !isReadOnly) {
val stream = ByteArrayOutputStream()
resource.compress(format, quality, stream)
tilesDao.createOrUpdate(RMapsTiles().also {
it.x = x
it.y = y
it.z = z
it.s = 0
it.image = stream.toByteArray()
})
}
return resource
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is RMapsBitmapFactory) return false
if (contentKey != other.contentKey) return false
if (x != other.x) return false
if (y != other.y) return false
if (z != other.z) return false
return true
}

override fun hashCode(): Int {
var result = contentKey.hashCode()
result = 31 * result + x
result = 31 * result + y
result = 31 * result + z
return result
}

override fun toString() = "RMapsBitmapFactory(contentKey=$contentKey, x=$x, y=$y, z=$z)"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package earth.worldwind.layer.rmaps

import android.graphics.Bitmap
import android.os.Build
import com.j256.ormlite.dao.Dao
import earth.worldwind.render.image.ImageSource

actual fun buildImageSource(
tilesDao: Dao<RMapsTiles, *>, readOnly: Boolean, contentKey: String, x: Int, y: Int, z: Int, imageFormat: String?
): ImageSource {
val format = when {
imageFormat.equals("image/jpeg", true) -> Bitmap.CompressFormat.JPEG
imageFormat.equals("image/png", true) -> Bitmap.CompressFormat.PNG
imageFormat.equals("image/webp", true) -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Bitmap.CompressFormat.WEBP_LOSSLESS
} else {
@Suppress("DEPRECATION")
Bitmap.CompressFormat.WEBP
}
else -> Bitmap.CompressFormat.PNG
}
return ImageSource.fromBitmapFactory(RMapsBitmapFactory(tilesDao, readOnly, contentKey, x, y, z, format))
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,24 @@ open class ATAKTileFactory(
protected val connectionSource = initConnection(contentPath, isReadOnly)
protected val tilesDao: Dao<ATAKTiles, Int> = DaoManager.createDao(connectionSource, ATAKTiles::class.java)
protected val metadataDao: Dao<ATAKMetadata, String> = DaoManager.createDao(connectionSource, ATAKMetadata::class.java)
//protected val catalogDao: Dao<AtakCatalog, Int> = DaoManager.createDao(connectionSource, AtakCatalog::class.java)
protected val contentFie = File(contentPath)
//protected val catalogDao: Dao<ATAKCatalog, Int> = DaoManager.createDao(connectionSource, ATAKCatalog::class.java)
protected val contentFile = File(contentPath)
override val contentType = if (tilesDao.isTableExists && metadataDao.isTableExists) "ATAK" else error("Not an ATAK map file")
override val contentKey = tilesDao.queryForFirst()?.provider ?: error("Empty cache file")
override val lastUpdateDate get() = Instant.fromEpochMilliseconds(contentFie.lastModified())
override val lastUpdateDate get() = Instant.fromEpochMilliseconds(contentFile.lastModified())
val srid = metadataDao.queryForId("srid")?.value?.toIntOrNull()
val isShutdown get() = !connectionSource.isOpen("")

fun shutdown() = connectionSource.close()

override suspend fun contentSize() = contentFie.length() // One file should contain one map
override suspend fun contentSize() = contentFile.length() // One file should contain one map

override suspend fun clearContent(deleteMetadata: Boolean) {
withContext(Dispatchers.IO) {
if (isReadOnly) error("Database is readonly!")
if (deleteMetadata) {
connectionSource.close()
contentFie.delete()
contentFile.delete()
} else if (tilesDao.isTableExists) tilesDao.deleteBuilder().delete() else Unit
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ import kotlinx.datetime.Instant
import java.io.File

expect fun buildImageSource(
tilesDao: Dao<MBTiles, Int>, readOnly: Boolean, contentKey: String, zoom: Int, column: Int, row: Int, imageFormat: String?
tilesDao: Dao<MBTiles, *>, readOnly: Boolean, contentKey: String, zoom: Int, column: Int, row: Int, imageFormat: String?
): ImageSource

open class MBTileFactory(final override val contentPath: String, val isReadOnly: Boolean) : CacheTileFactory {
protected val connectionSource = initConnection(contentPath, isReadOnly)
protected val tilesDao: Dao<MBTiles, Int> = DaoManager.createDao(connectionSource, MBTiles::class.java)
protected val tilesDao = DaoManager.createDao(connectionSource, MBTiles::class.java)
protected val metadataDao: Dao<MBTilesMetadata, String> = DaoManager.createDao(connectionSource, MBTilesMetadata::class.java)
protected val contentFie = File(contentPath)
protected val contentFile = File(contentPath)
override val contentType = if (metadataDao.isTableExists && tilesDao.isTableExists) "MBTiles" else error("Not an MBTiles map file")
override val contentKey = metadataDao.queryForId("name")?.value ?: error("Empty name!")
override val lastUpdateDate get() = Instant.fromEpochMilliseconds(contentFie.lastModified())
override val lastUpdateDate get() = Instant.fromEpochMilliseconds(contentFile.lastModified())
val boundingSector = metadataDao.queryForId("bounds")?.value?.let {
val box = it.split(",")
if (box.size < 4) return@let null
Expand All @@ -46,14 +46,14 @@ open class MBTileFactory(final override val contentPath: String, val isReadOnly:

fun shutdown() = connectionSource.close()

override suspend fun contentSize() = contentFie.length() // One file should contain one map
override suspend fun contentSize() = contentFile.length() // One file should contain one map

override suspend fun clearContent(deleteMetadata: Boolean) {
withContext(Dispatchers.IO) {
if (isReadOnly) error("Database is readonly!")
if (deleteMetadata) {
connectionSource.close()
contentFie.delete()
contentFile.delete()
} else if (tilesDao.isTableExists) tilesDao.deleteBuilder().delete() else Unit
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package earth.worldwind.layer.rmaps

import com.j256.ormlite.field.DataType
import com.j256.ormlite.field.DatabaseField
import com.j256.ormlite.table.DatabaseTable
import earth.worldwind.layer.rmaps.RMapsInfo.Companion.TABLE_NAME

@DatabaseTable(tableName = TABLE_NAME)
class RMapsInfo {
@DatabaseField(columnName = MIN_ZOOM, dataType = DataType.INTEGER)
var minzoom = 0
@DatabaseField(columnName = MAX_ZOOM, dataType = DataType.INTEGER)
var maxzoom = 0

companion object {
const val TABLE_NAME = "info"
const val MIN_ZOOM = "minzoom"
const val MAX_ZOOM = "maxzoom"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package earth.worldwind.layer.rmaps

import earth.worldwind.geom.Location
import earth.worldwind.layer.mercator.MercatorSector
import earth.worldwind.layer.mercator.MercatorTiledImageLayer
import earth.worldwind.layer.mercator.MercatorTiledSurfaceImage
import earth.worldwind.util.LevelSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

object RMapsLayerFactory {
suspend fun createLayer(
pathName: String, readOnly: Boolean = true, imageFormat: String? = null
) = withContext(Dispatchers.IO) {
val tileFactory = RMapsTileFactory(pathName, readOnly, imageFormat)
val tileOrigin = MercatorSector()
val sector = MercatorSector()
val levelSet = LevelSet(
sector = sector,
tileOrigin = tileOrigin,
firstLevelDelta = Location(tileOrigin.deltaLatitude, tileOrigin.deltaLongitude),
numLevels = tileFactory.numLevels,
tileWidth = 256,
tileHeight = 256,
levelOffset = tileFactory.levelOffset
)
MercatorTiledImageLayer(tileFactory.contentKey, MercatorTiledSurfaceImage(tileFactory, levelSet)).apply {
tiledSurfaceImage?.cacheTileFactory = tileFactory
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package earth.worldwind.layer.rmaps

import com.j256.ormlite.dao.Dao
import com.j256.ormlite.dao.DaoManager
import earth.worldwind.geom.Sector
import earth.worldwind.layer.mercator.MercatorImageTile
import earth.worldwind.layer.mercator.MercatorSector
import earth.worldwind.render.image.ImageSource
import earth.worldwind.util.CacheTileFactory
import earth.worldwind.util.Level
import earth.worldwind.util.ormlite.initConnection
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.datetime.Instant
import java.io.File

expect fun buildImageSource(
tilesDao: Dao<RMapsTiles, *>, readOnly: Boolean, contentKey: String, x: Int, y: Int, z: Int, imageFormat: String?
): ImageSource

open class RMapsTileFactory(
final override val contentPath: String, val isReadOnly: Boolean, protected val imageFormat: String?
) : CacheTileFactory {
protected val connectionSource = initConnection(contentPath, isReadOnly)
protected val tilesDao: Dao<RMapsTiles, *> = DaoManager.createDao(connectionSource, RMapsTiles::class.java)
protected val infoDao: Dao<RMapsInfo, *> = DaoManager.createDao(connectionSource, RMapsInfo::class.java)
protected val contentFile = File(contentPath)
override val contentType = if (tilesDao.isTableExists && infoDao.isTableExists) "RMaps" else error("Not an RMaps map file")
override val contentKey = contentFile.nameWithoutExtension
override val lastUpdateDate get() = Instant.fromEpochMilliseconds(contentFile.lastModified())
val numLevels get() = infoDao.queryForFirst()?.minzoom?.let { 17 - it + 1 } ?: 18
val levelOffset get() = infoDao.queryForFirst()?.maxzoom?.let { 17 - it } ?: 0
val isShutdown get() = !connectionSource.isOpen("")

fun shutdown() = connectionSource.close()

override suspend fun contentSize() = contentFile.length() // One file should contain one map

override suspend fun clearContent(deleteMetadata: Boolean) {
withContext(Dispatchers.IO) {
if (isReadOnly) error("Database is readonly!")
if (deleteMetadata) {
connectionSource.close()
contentFile.delete()
} else if (tilesDao.isTableExists) tilesDao.deleteBuilder().delete() else Unit
}
}

override fun createTile(sector: Sector, level: Level, row: Int, column: Int) =
buildTile(sector, level, row, column).apply {
imageSource = buildImageSource(
tilesDao, isReadOnly, contentKey, column, row, 17 - level.levelNumber, imageFormat
).also { it.postprocessor = this }
}

protected open fun buildTile(sector: Sector, level: Level, row: Int, column: Int) = if (sector is MercatorSector) {
MercatorImageTile(sector, level, row, column)
} else {
error("Only Mercator sector is supported!")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package earth.worldwind.layer.rmaps

import com.j256.ormlite.field.DataType
import com.j256.ormlite.field.DatabaseField
import com.j256.ormlite.table.DatabaseTable
import earth.worldwind.layer.rmaps.RMapsTiles.Companion.TABLE_NAME

@DatabaseTable(tableName = TABLE_NAME)
class RMapsTiles {
@DatabaseField(columnName = X, dataType = DataType.INTEGER, uniqueCombo = true)
var x = 0
@DatabaseField(columnName = Y, dataType = DataType.INTEGER, uniqueCombo = true)
var y = 0
@DatabaseField(columnName = Z, dataType = DataType.INTEGER, uniqueCombo = true)
var z = 0
@DatabaseField(columnName = S, dataType = DataType.INTEGER, uniqueCombo = true)
var s = 0
@DatabaseField(columnName = IMAGE, dataType = DataType.BYTE_ARRAY)
var image: ByteArray? = null

companion object {
const val TABLE_NAME = "tiles"
const val X = "x"
const val Y = "y"
const val Z = "z"
const val S = "s"
const val IMAGE = "image"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import com.j256.ormlite.dao.Dao
import earth.worldwind.render.image.ImageSource

actual fun buildImageSource(
tilesDao: Dao<MBTiles, Int>, readOnly: Boolean, contentKey: String, zoom: Int, column: Int, row: Int, imageFormat: String?
tilesDao: Dao<MBTiles, *>, readOnly: Boolean, contentKey: String, zoom: Int, column: Int, row: Int, imageFormat: String?
): ImageSource = TODO("Not yet implemented")
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package earth.worldwind.layer.rmaps

import com.j256.ormlite.dao.Dao
import earth.worldwind.render.image.ImageSource

actual fun buildImageSource(
tilesDao: Dao<RMapsTiles, *>, readOnly: Boolean, contentKey: String, x: Int, y: Int, z: Int, imageFormat: String?
): ImageSource = TODO("Not yet implemented")

0 comments on commit 32d46ad

Please sign in to comment.