Skip to content

Commit 3626f92

Browse files
github-actions[bot]klokantechsysadminsasaprodribaba
authored
RD-1378: Add addImage method to the Map - Kotlin (#89)
Co-authored-by: klokantechsysadmin <[email protected]> Co-authored-by: sasaprodribaba <[email protected]>
1 parent e0b53c3 commit 3626f92

File tree

6 files changed

+172
-0
lines changed

6 files changed

+172
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (c) 2025, MapTiler
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD 3-Clause
5+
*/
6+
7+
package com.maptiler.maptilersdk.commands.style
8+
9+
import android.graphics.Bitmap
10+
import com.maptiler.maptilersdk.bridge.JSString
11+
import com.maptiler.maptilersdk.bridge.MTBridge
12+
import com.maptiler.maptilersdk.bridge.MTCommand
13+
import com.maptiler.maptilersdk.helpers.ImageHelper
14+
import com.maptiler.maptilersdk.helpers.JsonConfig
15+
import com.maptiler.maptilersdk.map.style.image.MTAddImageOptions
16+
import kotlinx.serialization.Serializable
17+
import kotlinx.serialization.encodeToString
18+
19+
internal data class AddImage(
20+
val identifier: String,
21+
val image: Bitmap,
22+
val options: MTAddImageOptions? = null,
23+
) : MTCommand {
24+
override val isPrimitiveReturnType: Boolean = false
25+
26+
override fun toJS(): JSString {
27+
val encoded = ImageHelper.encodeImageWithMime(image)
28+
val dataUri = ImageHelper.getEncodedString(encoded)
29+
val sanitizedIdentifier = identifier.replace("\\", "\\\\").replace("'", "\\'")
30+
val optionsJson =
31+
options?.let {
32+
val surrogate =
33+
AddImageOptionsSurrogate(
34+
pixelRatio = it.pixelRatio,
35+
sdf = it.sdf,
36+
)
37+
JsonConfig.json.encodeToString(surrogate)
38+
}
39+
40+
val addImageCall =
41+
if (optionsJson != null) {
42+
"${MTBridge.MAP_OBJECT}.style.addImage('$sanitizedIdentifier', __mtImg, $optionsJson);"
43+
} else {
44+
"${MTBridge.MAP_OBJECT}.style.addImage('$sanitizedIdentifier', __mtImg);"
45+
}
46+
47+
return """
48+
(function() {
49+
const __mtImg = new Image();
50+
__mtImg.src = '$dataUri';
51+
__mtImg.onload = function() {
52+
$addImageCall
53+
};
54+
})();
55+
""".trimIndent()
56+
}
57+
}
58+
59+
@Serializable
60+
private data class AddImageOptionsSurrogate(
61+
val pixelRatio: Double? = null,
62+
val sdf: Boolean? = null,
63+
)

MapTilerSDK/src/main/java/com/maptiler/maptilersdk/helpers/ImageHelper.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ internal object ImageHelper {
3838
return EncodedImage(base64 = base64, mimeType = mime)
3939
}
4040

41+
/**
42+
* Returns a data URI for the given encoded image payload.
43+
*/
44+
fun getEncodedString(encodedImage: EncodedImage): String = "data:${encodedImage.mimeType};base64,${encodedImage.base64}"
45+
4146
/**
4247
* Deprecated: prefer [encodeImageWithMime]. Kept for tests/backward-compatibility.
4348
*/

MapTilerSDK/src/main/java/com/maptiler/maptilersdk/map/style/MTStyle.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
package com.maptiler.maptilersdk.map.style
88

9+
import android.graphics.Bitmap
910
import com.maptiler.maptilersdk.annotations.MTMarker
1011
import com.maptiler.maptilersdk.annotations.MTTextPopup
1112
import com.maptiler.maptilersdk.bridge.MTBridge
1213
import com.maptiler.maptilersdk.bridge.MTError
14+
import com.maptiler.maptilersdk.map.style.image.MTAddImageOptions
1315
import com.maptiler.maptilersdk.map.style.layer.MTLayer
1416
import com.maptiler.maptilersdk.map.style.source.MTSource
1517
import com.maptiler.maptilersdk.map.types.MTLanguage
@@ -115,6 +117,19 @@ class MTStyle(
115117
*/
116118
override fun removeTextPopup(popup: MTTextPopup) = stylableWorker.removeTextPopup(popup)
117119

120+
/**
121+
* Registers an image asset that can be referenced from the style.
122+
*
123+
* @param identifier Unique name for the image in the style registry.
124+
* @param image Bitmap containing the image data.
125+
* @param options Optional image configuration such as pixel ratio or SDF toggle.
126+
*/
127+
fun addImage(
128+
identifier: String,
129+
image: Bitmap,
130+
options: MTAddImageOptions? = null,
131+
) = stylableWorker.addImage(identifier, image, options)
132+
118133
/**
119134
* Adds a layer to the map.
120135
*
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (c) 2025, MapTiler
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD 3-Clause
5+
*/
6+
7+
package com.maptiler.maptilersdk.map.style.image
8+
9+
/**
10+
* Options used when registering an image in the current map style.
11+
*
12+
* @param sdf Whether the image should be interpreted as a signed distance field.
13+
* @param pixelRatio Override pixel ratio for the image. Must be greater than 0 when provided.
14+
*/
15+
data class MTAddImageOptions(
16+
val sdf: Boolean? = null,
17+
val pixelRatio: Double? = null,
18+
) {
19+
init {
20+
if (pixelRatio != null && pixelRatio <= 0.0) {
21+
throw IllegalArgumentException("pixelRatio must be greater than 0.0")
22+
}
23+
}
24+
}

MapTilerSDK/src/main/java/com/maptiler/maptilersdk/map/workers/stylable/StylableWorker.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

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

9+
import android.graphics.Bitmap
910
import com.maptiler.maptilersdk.annotations.MTMarker
1011
import com.maptiler.maptilersdk.annotations.MTTextPopup
1112
import com.maptiler.maptilersdk.bridge.MTBridge
@@ -15,6 +16,7 @@ import com.maptiler.maptilersdk.commands.annotations.AddTextPopup
1516
import com.maptiler.maptilersdk.commands.annotations.RemoveMarker
1617
import com.maptiler.maptilersdk.commands.annotations.RemoveTextPopup
1718
import com.maptiler.maptilersdk.commands.misc.AddLogoControl
19+
import com.maptiler.maptilersdk.commands.style.AddImage
1820
import com.maptiler.maptilersdk.commands.style.AddLayer
1921
import com.maptiler.maptilersdk.commands.style.AddSource
2022
import com.maptiler.maptilersdk.commands.style.DisableHalo
@@ -47,6 +49,7 @@ import com.maptiler.maptilersdk.map.options.MTHalo
4749
import com.maptiler.maptilersdk.map.options.MTSpace
4850
import com.maptiler.maptilersdk.map.style.MTMapReferenceStyle
4951
import com.maptiler.maptilersdk.map.style.MTMapStyleVariant
52+
import com.maptiler.maptilersdk.map.style.image.MTAddImageOptions
5053
import com.maptiler.maptilersdk.map.style.layer.MTLayer
5154
import com.maptiler.maptilersdk.map.style.source.MTSource
5255
import com.maptiler.maptilersdk.map.types.MTLanguage
@@ -200,6 +203,18 @@ internal class StylableWorker(
200203
}
201204
}
202205

206+
fun addImage(
207+
identifier: String,
208+
image: Bitmap,
209+
options: MTAddImageOptions? = null,
210+
) {
211+
scope.launch {
212+
bridge.execute(
213+
AddImage(identifier, image, options),
214+
)
215+
}
216+
}
217+
203218
fun removeLayer(layer: MTLayer) {
204219
scope.launch {
205220
bridge.execute(

MapTilerSDK/src/test/java/com/maptiler/maptilersdk/StyleAndCommandsTests.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ package com.maptiler.maptilersdk
88

99
import android.graphics.Bitmap
1010
import com.maptiler.maptilersdk.bridge.MTBridge
11+
import com.maptiler.maptilersdk.bridge.MTBridgeReturnType
12+
import com.maptiler.maptilersdk.bridge.MTCommand
13+
import com.maptiler.maptilersdk.bridge.MTCommandExecutable
14+
import com.maptiler.maptilersdk.commands.style.AddImage
1115
import com.maptiler.maptilersdk.commands.style.AddSource
1216
import com.maptiler.maptilersdk.commands.style.DisableTerrain
1317
import com.maptiler.maptilersdk.commands.style.EnableGlobeProjection
@@ -18,13 +22,18 @@ import com.maptiler.maptilersdk.commands.style.SetDataToSource
1822
import com.maptiler.maptilersdk.commands.style.SetTilesToSource
1923
import com.maptiler.maptilersdk.helpers.EncodedImage
2024
import com.maptiler.maptilersdk.helpers.ImageHelper
25+
import com.maptiler.maptilersdk.map.style.MTMapReferenceStyle
26+
import com.maptiler.maptilersdk.map.style.MTStyle
27+
import com.maptiler.maptilersdk.map.style.image.MTAddImageOptions
2128
import com.maptiler.maptilersdk.map.style.layer.symbol.MTSymbolLayer
2229
import com.maptiler.maptilersdk.map.style.source.MTGeoJSONSource
2330
import com.maptiler.maptilersdk.map.style.source.MTVectorTileSource
2431
import io.mockk.every
2532
import io.mockk.mockk
2633
import io.mockk.mockkObject
2734
import junit.framework.TestCase.assertEquals
35+
import kotlinx.coroutines.CoroutineScope
36+
import kotlinx.coroutines.Dispatchers
2837
import org.junit.Assert.assertTrue
2938
import org.junit.Test
3039
import java.net.URL
@@ -181,4 +190,45 @@ class StyleAndCommandsTests {
181190
assertTrue(js.contains("\"text-size\":12.0"))
182191
assertTrue(js.contains("\"filter\":[\"has\",\"point_count\"]"))
183192
}
193+
194+
@Test fun addImageToJS_EncodesBitmapAndOptions() {
195+
val bmp = mockk<Bitmap>()
196+
every { bmp.hasAlpha() } returns true
197+
every { bmp.compress(any(), any(), any()) } returns true
198+
199+
mockkObject(ImageHelper)
200+
every { ImageHelper.encodeImageWithMime(any()) } returns EncodedImage("CCC", "image/png")
201+
every { ImageHelper.getEncodedString(any()) } returns "data:image/png;base64,CCC"
202+
203+
val options = MTAddImageOptions(sdf = true, pixelRatio = 2.0)
204+
val js =
205+
AddImage("poi-icon", bmp, options).toJS()
206+
207+
assertTrue(js.contains("${MTBridge.MAP_OBJECT}.style.addImage('poi-icon'"))
208+
assertTrue(js.contains("data:image/png;base64,CCC"))
209+
assertTrue(js.contains("\"sdf\":true"))
210+
assertTrue(js.contains("\"pixelRatio\":2.0"))
211+
}
212+
213+
@Test fun mtStyleAddImage_DelegatesToBridge() {
214+
val recordedCommands = mutableListOf<MTCommand>()
215+
val bridge =
216+
MTBridge(
217+
object : MTCommandExecutable {
218+
override suspend fun execute(command: MTCommand): MTBridgeReturnType {
219+
recordedCommands.add(command)
220+
return MTBridgeReturnType.Null
221+
}
222+
},
223+
)
224+
val style = MTStyle(MTMapReferenceStyle.STREETS)
225+
style.initWorker(bridge, CoroutineScope(Dispatchers.Unconfined))
226+
227+
val bmp = mockk<Bitmap>(relaxed = true)
228+
229+
style.addImage("poi-icon", bmp)
230+
231+
assertEquals(1, recordedCommands.size)
232+
assertTrue(recordedCommands.first() is AddImage)
233+
}
184234
}

0 commit comments

Comments
 (0)