Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2025, MapTiler
* All rights reserved.
* SPDX-License-Identifier: BSD 3-Clause
*/

package com.maptiler.maptilersdk.commands.style

import android.graphics.Bitmap
import com.maptiler.maptilersdk.bridge.JSString
import com.maptiler.maptilersdk.bridge.MTBridge
import com.maptiler.maptilersdk.bridge.MTCommand
import com.maptiler.maptilersdk.helpers.ImageHelper
import com.maptiler.maptilersdk.helpers.JsonConfig
import com.maptiler.maptilersdk.map.style.image.MTAddImageOptions
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString

internal data class AddImage(
val identifier: String,
val image: Bitmap,
val options: MTAddImageOptions? = null,
) : MTCommand {
override val isPrimitiveReturnType: Boolean = false

override fun toJS(): JSString {
val encoded = ImageHelper.encodeImageWithMime(image)
val dataUri = ImageHelper.getEncodedString(encoded)
val sanitizedIdentifier = identifier.replace("\\", "\\\\").replace("'", "\\'")
val optionsJson =
options?.let {
val surrogate =
AddImageOptionsSurrogate(
pixelRatio = it.pixelRatio,
sdf = it.sdf,
)
JsonConfig.json.encodeToString(surrogate)
}

val addImageCall =
if (optionsJson != null) {
"${MTBridge.MAP_OBJECT}.style.addImage('$sanitizedIdentifier', __mtImg, $optionsJson);"
} else {
"${MTBridge.MAP_OBJECT}.style.addImage('$sanitizedIdentifier', __mtImg);"
}

return """
(function() {
const __mtImg = new Image();
__mtImg.src = '$dataUri';
__mtImg.onload = function() {
$addImageCall
};
})();
""".trimIndent()
}
}

@Serializable
private data class AddImageOptionsSurrogate(
val pixelRatio: Double? = null,
val sdf: Boolean? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ internal object ImageHelper {
return EncodedImage(base64 = base64, mimeType = mime)
}

/**
* Returns a data URI for the given encoded image payload.
*/
fun getEncodedString(encodedImage: EncodedImage): String = "data:${encodedImage.mimeType};base64,${encodedImage.base64}"

/**
* Deprecated: prefer [encodeImageWithMime]. Kept for tests/backward-compatibility.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

package com.maptiler.maptilersdk.map.style

import android.graphics.Bitmap
import com.maptiler.maptilersdk.annotations.MTMarker
import com.maptiler.maptilersdk.annotations.MTTextPopup
import com.maptiler.maptilersdk.bridge.MTBridge
import com.maptiler.maptilersdk.bridge.MTError
import com.maptiler.maptilersdk.map.style.image.MTAddImageOptions
import com.maptiler.maptilersdk.map.style.layer.MTLayer
import com.maptiler.maptilersdk.map.style.source.MTSource
import com.maptiler.maptilersdk.map.types.MTLanguage
Expand Down Expand Up @@ -115,6 +117,19 @@ class MTStyle(
*/
override fun removeTextPopup(popup: MTTextPopup) = stylableWorker.removeTextPopup(popup)

/**
* Registers an image asset that can be referenced from the style.
*
* @param identifier Unique name for the image in the style registry.
* @param image Bitmap containing the image data.
* @param options Optional image configuration such as pixel ratio or SDF toggle.
*/
fun addImage(
identifier: String,
image: Bitmap,
options: MTAddImageOptions? = null,
) = stylableWorker.addImage(identifier, image, options)

/**
* Adds a layer to the map.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025, MapTiler
* All rights reserved.
* SPDX-License-Identifier: BSD 3-Clause
*/

package com.maptiler.maptilersdk.map.style.image

/**
* Options used when registering an image in the current map style.
*
* @param sdf Whether the image should be interpreted as a signed distance field.
* @param pixelRatio Override pixel ratio for the image. Must be greater than 0 when provided.
*/
data class MTAddImageOptions(
val sdf: Boolean? = null,
val pixelRatio: Double? = null,
) {
init {
if (pixelRatio != null && pixelRatio <= 0.0) {
throw IllegalArgumentException("pixelRatio must be greater than 0.0")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package com.maptiler.maptilersdk.map.workers.stylable

import android.graphics.Bitmap
import com.maptiler.maptilersdk.annotations.MTMarker
import com.maptiler.maptilersdk.annotations.MTTextPopup
import com.maptiler.maptilersdk.bridge.MTBridge
Expand All @@ -15,6 +16,7 @@ import com.maptiler.maptilersdk.commands.annotations.AddTextPopup
import com.maptiler.maptilersdk.commands.annotations.RemoveMarker
import com.maptiler.maptilersdk.commands.annotations.RemoveTextPopup
import com.maptiler.maptilersdk.commands.misc.AddLogoControl
import com.maptiler.maptilersdk.commands.style.AddImage
import com.maptiler.maptilersdk.commands.style.AddLayer
import com.maptiler.maptilersdk.commands.style.AddSource
import com.maptiler.maptilersdk.commands.style.DisableHalo
Expand Down Expand Up @@ -47,6 +49,7 @@ import com.maptiler.maptilersdk.map.options.MTHalo
import com.maptiler.maptilersdk.map.options.MTSpace
import com.maptiler.maptilersdk.map.style.MTMapReferenceStyle
import com.maptiler.maptilersdk.map.style.MTMapStyleVariant
import com.maptiler.maptilersdk.map.style.image.MTAddImageOptions
import com.maptiler.maptilersdk.map.style.layer.MTLayer
import com.maptiler.maptilersdk.map.style.source.MTSource
import com.maptiler.maptilersdk.map.types.MTLanguage
Expand Down Expand Up @@ -200,6 +203,18 @@ internal class StylableWorker(
}
}

fun addImage(
identifier: String,
image: Bitmap,
options: MTAddImageOptions? = null,
) {
scope.launch {
bridge.execute(
AddImage(identifier, image, options),
)
}
}

fun removeLayer(layer: MTLayer) {
scope.launch {
bridge.execute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ package com.maptiler.maptilersdk

import android.graphics.Bitmap
import com.maptiler.maptilersdk.bridge.MTBridge
import com.maptiler.maptilersdk.bridge.MTBridgeReturnType
import com.maptiler.maptilersdk.bridge.MTCommand
import com.maptiler.maptilersdk.bridge.MTCommandExecutable
import com.maptiler.maptilersdk.commands.style.AddImage
import com.maptiler.maptilersdk.commands.style.AddSource
import com.maptiler.maptilersdk.commands.style.DisableTerrain
import com.maptiler.maptilersdk.commands.style.EnableGlobeProjection
Expand All @@ -18,13 +22,18 @@ import com.maptiler.maptilersdk.commands.style.SetDataToSource
import com.maptiler.maptilersdk.commands.style.SetTilesToSource
import com.maptiler.maptilersdk.helpers.EncodedImage
import com.maptiler.maptilersdk.helpers.ImageHelper
import com.maptiler.maptilersdk.map.style.MTMapReferenceStyle
import com.maptiler.maptilersdk.map.style.MTStyle
import com.maptiler.maptilersdk.map.style.image.MTAddImageOptions
import com.maptiler.maptilersdk.map.style.layer.symbol.MTSymbolLayer
import com.maptiler.maptilersdk.map.style.source.MTGeoJSONSource
import com.maptiler.maptilersdk.map.style.source.MTVectorTileSource
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.junit.Assert.assertTrue
import org.junit.Test
import java.net.URL
Expand Down Expand Up @@ -181,4 +190,45 @@ class StyleAndCommandsTests {
assertTrue(js.contains("\"text-size\":12.0"))
assertTrue(js.contains("\"filter\":[\"has\",\"point_count\"]"))
}

@Test fun addImageToJS_EncodesBitmapAndOptions() {
val bmp = mockk<Bitmap>()
every { bmp.hasAlpha() } returns true
every { bmp.compress(any(), any(), any()) } returns true

mockkObject(ImageHelper)
every { ImageHelper.encodeImageWithMime(any()) } returns EncodedImage("CCC", "image/png")
every { ImageHelper.getEncodedString(any()) } returns "data:image/png;base64,CCC"

val options = MTAddImageOptions(sdf = true, pixelRatio = 2.0)
val js =
AddImage("poi-icon", bmp, options).toJS()

assertTrue(js.contains("${MTBridge.MAP_OBJECT}.style.addImage('poi-icon'"))
assertTrue(js.contains("data:image/png;base64,CCC"))
assertTrue(js.contains("\"sdf\":true"))
assertTrue(js.contains("\"pixelRatio\":2.0"))
}

@Test fun mtStyleAddImage_DelegatesToBridge() {
val recordedCommands = mutableListOf<MTCommand>()
val bridge =
MTBridge(
object : MTCommandExecutable {
override suspend fun execute(command: MTCommand): MTBridgeReturnType {
recordedCommands.add(command)
return MTBridgeReturnType.Null
}
},
)
val style = MTStyle(MTMapReferenceStyle.STREETS)
style.initWorker(bridge, CoroutineScope(Dispatchers.Unconfined))

val bmp = mockk<Bitmap>(relaxed = true)

style.addImage("poi-icon", bmp)

assertEquals(1, recordedCommands.size)
assertTrue(recordedCommands.first() is AddImage)
}
}