From 572a41d946f7e633d6ed714f67823dfb96f85cd9 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 8 Nov 2024 13:01:32 +0200 Subject: [PATCH] Improve usage of bitmap to mapbox image conversions (#2841) --- CHANGELOG.md | 5 +- .../examples/AnimatedImageSourceActivity.kt | 77 +++++++----------- .../examples/CircleLayerClusteringActivity.kt | 4 +- .../maps/testapp/examples/GesturesActivity.kt | 25 ++++-- .../testapp/examples/ImageSourceActivity.kt | 6 +- .../examples/RuntimeStylingActivity.kt | 5 +- .../examples/SpaceStationLocationActivity.kt | 8 +- .../examples/TintFillPatternActivity.kt | 7 +- .../AnimatePointAnnotationActivity.kt | 71 ++++++++-------- .../java/RuntimeStylingJavaActivity.java | 3 + .../MovingIconWithTrailingLineActivity.kt | 10 ++- .../PointAnnotationActivity.kt | 81 +++++++------------ .../infowindow/InfoWindowActivity.kt | 8 +- .../DynamicViewAnnotationActivity.kt | 2 +- ...ewAnnotationWithPointAnnotationActivity.kt | 17 ++-- .../style/CustomRasterSourceActivity.kt | 38 +++------ .../terrain3D/SantaCatalinaActivity.kt | 10 +-- .../mapbox/maps/testapp/utils/BitmapUtils.kt | 13 ++- .../style/AnimatedImageSourceActivity.kt | 22 ++--- .../examples/style/ImageSourceActivity.kt | 8 +- .../extension/compose/style/StyleImage.kt | 3 + .../maps/extension/style/image/ImageExt.kt | 1 + .../style/image/ImageExtensionImpl.kt | 2 + .../extension/style/image/NinePatchUtils.kt | 8 ++ .../extension/style/sources/ImageSourceExt.kt | 5 ++ .../style/sources/CustomRasterSourceTest.kt | 2 + .../annotation/AnnotationManagerImpl.kt | 24 +++--- .../annotation/generated/PointAnnotation.kt | 11 ++- .../generated/PointAnnotationManager.kt | 12 +-- .../generated/CircleAnnotationManagerTest.kt | 3 + .../generated/PointAnnotationManagerTest.kt | 69 +++++++++++++--- sdk-base/api/Release/metalava.txt | 4 +- .../java/com/mapbox/maps/ExtensionUtils.kt | 4 + .../java/com/mapbox/maps/MapboxDelicateApi.kt | 2 +- .../com/mapbox/maps/MapboxStyleManager.kt | 1 + 35 files changed, 308 insertions(+), 263 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7269a1d755..54c88901a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,12 @@ Mapbox welcomes participation and contributions from everyone. * Change the signature of experimental `MapboxMap.queryRenderedFeatures(RenderedQueryGeometry, TypedFeaturesetDescriptor, Value?, QueryRenderedFeaturesetFeaturesCallback)` to `MapboxMap.queryRenderedFeatures(TypedFeaturesetDescriptor, RenderedQueryGeometry?, Value?, QueryRenderedFeaturesetFeaturesCallback)`. `RenderedQueryGeometry` being NULL is equivalent to passing a bounding box encompassing the entire map viewport. * [compose] Change the signature of experimental `MapState.queryRenderedFeatures(RenderedQueryGeometry, TypedFeaturesetDescriptor, Expression?): List` to `MapState.queryRenderedFeatures(TypedFeaturesetDescriptor, RenderedQueryGeometry?, Expression?): List`. `RenderedQueryGeometry` being NULL is equivalent to passing a bounding box encompassing the entire map viewport. +## Features ✨ and improvements 🏁 +* Annotate `Bitmap.toMapboxImage()` and related as delicate API due to its native memory allocation. + ## Bug fixes 🐞 * Disable false-positive lint error "Incorrect number of expressions". - +* Fix possible out of memory in native heap during annotation manager annotation updates (`AnnotationManager.update(...)`). # 11.7.2 November 05, 2024 diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/AnimatedImageSourceActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/AnimatedImageSourceActivity.kt index 44bfe37185..e0e04c6523 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/AnimatedImageSourceActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/AnimatedImageSourceActivity.kt @@ -1,12 +1,12 @@ package com.mapbox.maps.testapp.examples -import android.content.Context -import android.graphics.Bitmap import android.os.Bundle -import android.os.Handler -import android.os.Looper import androidx.appcompat.app.AppCompatActivity -import com.mapbox.maps.MapboxMap +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.mapbox.maps.Image +import com.mapbox.maps.MapboxDelicateApi import com.mapbox.maps.Style import com.mapbox.maps.extension.style.layers.generated.rasterLayer import com.mapbox.maps.extension.style.sources.generated.ImageSource @@ -17,6 +17,10 @@ import com.mapbox.maps.extension.style.style import com.mapbox.maps.testapp.R import com.mapbox.maps.testapp.databinding.ActivityAnimatedImagesourceBinding import com.mapbox.maps.testapp.utils.BitmapUtils.bitmapFromDrawableRes +import com.mapbox.maps.toMapboxImage +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch /** * Load a raster image to a style using ImageSource and display it on a map as @@ -24,15 +28,12 @@ import com.mapbox.maps.testapp.utils.BitmapUtils.bitmapFromDrawableRes */ class AnimatedImageSourceActivity : AppCompatActivity() { - private val handler: Handler = Handler(Looper.getMainLooper()) - private lateinit var mapboxMap: MapboxMap - private lateinit var runnable: Runnable - + @OptIn(MapboxDelicateApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityAnimatedImagesourceBinding.inflate(layoutInflater) setContentView(binding.root) - mapboxMap = binding.mapView.mapboxMap + val mapboxMap = binding.mapView.mapboxMap mapboxMap.loadStyle( style(style = Style.STANDARD) { +imageSource(ID_IMAGE_SOURCE) { @@ -48,49 +49,27 @@ class AnimatedImageSourceActivity : AppCompatActivity() { +rasterLayer(ID_IMAGE_LAYER, ID_IMAGE_SOURCE) { } } ) - } - - override fun onStart() { - super.onStart() + val drawables: List = listOf( + bitmapFromDrawableRes(R.drawable.southeast_radar_0).toMapboxImage(), + bitmapFromDrawableRes(R.drawable.southeast_radar_1).toMapboxImage(), + bitmapFromDrawableRes(R.drawable.southeast_radar_2).toMapboxImage(), + bitmapFromDrawableRes(R.drawable.southeast_radar_3).toMapboxImage(), + ) + var drawableIndex = 0 mapboxMap.getStyle { val imageSource: ImageSource = it.getSourceAs(ID_IMAGE_SOURCE)!! - runnable = RefreshImageRunnable(applicationContext, imageSource, handler) - handler.postDelayed(runnable, 100) - } - } - - override fun onStop() { - super.onStop() - if (::runnable.isInitialized) { - handler.removeCallbacks(runnable) - } - } - - private class RefreshImageRunnable constructor( - appContext: Context, - private val imageSource: ImageSource, - private val handler: Handler - ) : - Runnable { - private val drawables: Array = arrayOfNulls(4) - private var drawableIndex: Int - - override fun run() { - drawables[drawableIndex++]?.let { bitmap -> - imageSource.updateImage(bitmap) - if (drawableIndex > 3) { - drawableIndex = 0 + // Create a new coroutine in the lifecycleScope + lifecycleScope.launch { + // repeatOnLifecycle launches the block in a new coroutine every time the + // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED. + repeatOnLifecycle(Lifecycle.State.STARTED) { + while (isActive) { + imageSource.updateImage(drawables[drawableIndex++]) + drawableIndex %= drawables.size + delay(1000L) + } } } - handler.postDelayed(this, 1000) - } - - init { - drawables[0] = bitmapFromDrawableRes(appContext, R.drawable.southeast_radar_0) - drawables[1] = bitmapFromDrawableRes(appContext, R.drawable.southeast_radar_1) - drawables[2] = bitmapFromDrawableRes(appContext, R.drawable.southeast_radar_2) - drawables[3] = bitmapFromDrawableRes(appContext, R.drawable.southeast_radar_3) - drawableIndex = 1 } } diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/CircleLayerClusteringActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/CircleLayerClusteringActivity.kt index 99975826f8..b37aa18e74 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/CircleLayerClusteringActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/CircleLayerClusteringActivity.kt @@ -58,9 +58,7 @@ class CircleLayerClusteringActivity : AppCompatActivity() { addClusteredGeoJsonSource(it) - bitmapFromDrawableRes(this, R.drawable.ic_cross)?.let { bitmap -> - it.addImage(CROSS_ICON_ID, bitmap, true) - } + it.addImage(CROSS_ICON_ID, bitmapFromDrawableRes(R.drawable.ic_cross), true) Toast.makeText( this@CircleLayerClusteringActivity, diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/GesturesActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/GesturesActivity.kt index b748373915..76c66cbc55 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/GesturesActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/GesturesActivity.kt @@ -4,7 +4,12 @@ import android.annotation.SuppressLint import android.os.Bundle import android.os.Handler import android.os.Looper -import android.view.* +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver import android.widget.RelativeLayout import android.widget.TextView import androidx.annotation.ColorInt @@ -12,10 +17,13 @@ import androidx.annotation.IntDef import androidx.annotation.NonNull import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.mapbox.android.gestures.* +import com.mapbox.android.gestures.AndroidGesturesManager +import com.mapbox.android.gestures.MoveGestureDetector +import com.mapbox.android.gestures.RotateGestureDetector +import com.mapbox.android.gestures.ShoveGestureDetector +import com.mapbox.android.gestures.StandardScaleGestureDetector import com.mapbox.geojson.Point import com.mapbox.maps.CameraOptions import com.mapbox.maps.MapboxMap @@ -25,10 +33,15 @@ import com.mapbox.maps.plugin.annotation.annotations import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager -import com.mapbox.maps.plugin.gestures.* +import com.mapbox.maps.plugin.gestures.GesturesPlugin +import com.mapbox.maps.plugin.gestures.OnMoveListener +import com.mapbox.maps.plugin.gestures.OnRotateListener +import com.mapbox.maps.plugin.gestures.OnScaleListener +import com.mapbox.maps.plugin.gestures.OnShoveListener +import com.mapbox.maps.plugin.gestures.gestures import com.mapbox.maps.testapp.R import com.mapbox.maps.testapp.databinding.ActivityGesturesBinding -import java.util.* +import com.mapbox.maps.testapp.utils.BitmapUtils.bitmapFromDrawableRes /** * Test activity showcasing APIs around gestures implementation. @@ -138,7 +151,7 @@ class GesturesActivity : AppCompatActivity() { .build() ) mapboxMap.loadStyle(Style.STANDARD) { - it.addImage(MARKER_IMAGE_ID, ContextCompat.getDrawable(this, R.drawable.ic_red_marker)!!.toBitmap()) + it.addImage(MARKER_IMAGE_ID, bitmapFromDrawableRes(R.drawable.ic_red_marker)) } binding.mapView.waitForLayout { diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/ImageSourceActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/ImageSourceActivity.kt index 06c66658bd..9e160a6c29 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/ImageSourceActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/ImageSourceActivity.kt @@ -36,10 +36,8 @@ class ImageSourceActivity : AppCompatActivity() { +rasterLayer(ID_IMAGE_LAYER, ID_IMAGE_SOURCE) {} } ) { - bitmapFromDrawableRes(this, R.drawable.miami_beach)?.let { bitmap -> - val imageSource: ImageSource = it.getSourceAs(ID_IMAGE_SOURCE)!! - imageSource.updateImage(bitmap) - } + val imageSource: ImageSource = it.getSourceAs(ID_IMAGE_SOURCE)!! + imageSource.updateImage(bitmapFromDrawableRes(R.drawable.miami_beach)) } } diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/RuntimeStylingActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/RuntimeStylingActivity.kt index 2e0711d09a..0a172149b4 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/RuntimeStylingActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/RuntimeStylingActivity.kt @@ -329,12 +329,13 @@ class RuntimeStylingActivity : AppCompatActivity() { style.addLayer(raster) } + @OptIn(MapboxDelicateApi::class) private fun addLayerWithoutStyleExtension(style: Style) { - val bitmap = ContextCompat.getDrawable(this, R.drawable.android_symbol)?.toBitmap(64, 64) + val bitmap = ContextCompat.getDrawable(this, R.drawable.android_symbol)!!.toBitmap(64, 64) val expected = style.addStyleImage( "myImage", 1f, - bitmap!!.toMapboxImage(), + bitmap.toMapboxImage(), false, mutableListOf(), mutableListOf(), diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/SpaceStationLocationActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/SpaceStationLocationActivity.kt index 462315d602..de8b7afecb 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/SpaceStationLocationActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/SpaceStationLocationActivity.kt @@ -9,13 +9,19 @@ import androidx.appcompat.app.AppCompatActivity import com.mapbox.geojson.Feature import com.mapbox.geojson.FeatureCollection import com.mapbox.geojson.Point -import com.mapbox.maps.* +import com.mapbox.maps.CameraOptions +import com.mapbox.maps.MapInitOptions +import com.mapbox.maps.MapView +import com.mapbox.maps.MapboxMap +import com.mapbox.maps.Style import com.mapbox.maps.extension.style.layers.addLayer import com.mapbox.maps.extension.style.layers.generated.symbolLayer import com.mapbox.maps.extension.style.sources.addSource import com.mapbox.maps.extension.style.sources.generated.GeoJsonSource import com.mapbox.maps.extension.style.sources.generated.geoJsonSource import com.mapbox.maps.extension.style.sources.getSource +import com.mapbox.maps.logE +import com.mapbox.maps.logW import com.mapbox.maps.plugin.animation.MapAnimationOptions.Companion.mapAnimationOptions import com.mapbox.maps.plugin.animation.flyTo import com.mapbox.maps.testapp.R diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/TintFillPatternActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/TintFillPatternActivity.kt index 29a8c1ff50..dcfe1caf04 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/TintFillPatternActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/TintFillPatternActivity.kt @@ -1,6 +1,11 @@ package com.mapbox.maps.testapp.examples -import android.graphics.* +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter import android.os.Bundle import androidx.annotation.ColorInt import androidx.appcompat.app.AppCompatActivity diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/annotation/AnimatePointAnnotationActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/annotation/AnimatePointAnnotationActivity.kt index a139e8d463..1d954fc014 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/annotation/AnimatePointAnnotationActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/annotation/AnimatePointAnnotationActivity.kt @@ -8,16 +8,23 @@ import android.view.animation.LinearInterpolator import androidx.appcompat.app.AppCompatActivity import androidx.core.animation.addListener import com.mapbox.geojson.Point -import com.mapbox.maps.* +import com.mapbox.maps.CoordinateBounds +import com.mapbox.maps.MapLoaded +import com.mapbox.maps.MapLoadedCallback +import com.mapbox.maps.MapboxMap import com.mapbox.maps.dsl.cameraOptions import com.mapbox.maps.extension.style.layers.properties.generated.IconPitchAlignment import com.mapbox.maps.plugin.annotation.annotations -import com.mapbox.maps.plugin.annotation.generated.* +import com.mapbox.maps.plugin.annotation.generated.PointAnnotation +import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager +import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions +import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager import com.mapbox.maps.testapp.R import com.mapbox.maps.testapp.databinding.ActivityAnnotationBinding import com.mapbox.maps.testapp.utils.BitmapUtils.bitmapFromDrawableRes +import com.mapbox.maps.toCameraOptions import com.mapbox.turf.TurfMeasurement -import java.util.* +import java.util.Random /** * Example showing how to add point annotations and animate them @@ -49,45 +56,32 @@ class AnimatePointAnnotationActivity : AppCompatActivity(), MapLoadedCallback { zoom(12.0) } ) - loadStyle(Style.STANDARD) + subscribeMapLoaded(this@AnimatePointAnnotationActivity) } - mapboxMap.subscribeMapLoaded(this@AnimatePointAnnotationActivity) binding.deleteAll.visibility = View.GONE binding.changeStyle.visibility = View.GONE + binding.changeSlot.visibility = View.GONE } override fun run(eventData: MapLoaded) { pointAnnotationManager = binding.mapView.annotations.createPointAnnotationManager().apply { - this.iconPitchAlignment = IconPitchAlignment.MAP - bitmapFromDrawableRes( - this@AnimatePointAnnotationActivity, - R.drawable.ic_car_top - )?.let { - val noAnimationOptionList = mutableListOf() - for (i in 0 until noAnimateCarNum) { - noAnimationOptionList.add( - PointAnnotationOptions() - .withPoint(getPointInBounds()) - .withIconImage(it) - ) - } - create(noAnimationOptionList) + iconPitchAlignment = IconPitchAlignment.MAP + val carTop = bitmapFromDrawableRes(R.drawable.ic_car_top) + val noAnimationOptionList = List(noAnimateCarNum) { + PointAnnotationOptions() + .withPoint(getPointInBounds()) + .withIconImage(carTop) } - bitmapFromDrawableRes( - this@AnimatePointAnnotationActivity, - R.drawable.ic_taxi_top - )?.let { - val animationOptionList = mutableListOf() - for (i in 0 until animateCarNum) { - animationOptionList.add( - PointAnnotationOptions() - .withPoint(getPointInBounds()) - .withIconImage(it) - ) - } - animateCarList = create(animationOptionList) + create(noAnimationOptionList) + + val taxiTop = bitmapFromDrawableRes(R.drawable.ic_taxi_top) + val animationOptionList = List(animateCarNum) { + PointAnnotationOptions() + .withPoint(getPointInBounds()) + .withIconImage(taxiTop) } + animateCarList = create(animationOptionList) } animateCars() } @@ -99,20 +93,19 @@ class AnimatePointAnnotationActivity : AppCompatActivity(), MapLoadedCallback { private fun animateCars() { cleanAnimation() - for (i in 0 until animateCarNum) { + val carEvaluator = CarEvaluator() + animateCarList.forEach { animatedCar -> val nextPoint = getPointInBounds() val animator = ValueAnimator.ofObject( - CarEvaluator(), - animateCarList[i].point, + carEvaluator, + animatedCar.point, nextPoint ).setDuration(animateDuration) - animateCarList[i].iconRotate = TurfMeasurement.bearing(animateCarList[i].point, nextPoint) + animatedCar.iconRotate = TurfMeasurement.bearing(animatedCar.point, nextPoint) animator.interpolator = LinearInterpolator() animator.addUpdateListener { valueAnimator -> - (valueAnimator.animatedValue as Point).let { - animateCarList[i].point = it - } + animatedCar.point = valueAnimator.animatedValue as Point } animator.start() animators.add(animator) diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/java/RuntimeStylingJavaActivity.java b/app/src/main/java/com/mapbox/maps/testapp/examples/java/RuntimeStylingJavaActivity.java index bb902481e5..8ccdd9fa91 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/java/RuntimeStylingJavaActivity.java +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/java/RuntimeStylingJavaActivity.java @@ -14,6 +14,7 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; +import androidx.annotation.OptIn; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableKt; @@ -24,6 +25,7 @@ import com.mapbox.geojson.FeatureCollection; import com.mapbox.maps.ExtensionUtils; import com.mapbox.maps.MapView; +import com.mapbox.maps.MapboxDelicateApi; import com.mapbox.maps.MapboxMap; import com.mapbox.maps.Style; import com.mapbox.maps.extension.style.expressions.generated.Expression; @@ -342,6 +344,7 @@ private void addRasterLayer(Style style) { LayerUtils.addLayer(style, raster); } + @OptIn(markerClass = MapboxDelicateApi.class) private void addLayerWithoutStyleExtension(Style style) { final Drawable drawable = ContextCompat.getDrawable(this, R.drawable.android_symbol); final Bitmap bitmap = DrawableKt.toBitmap(drawable, 64, 64, null); diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/linesandpolygons/MovingIconWithTrailingLineActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/linesandpolygons/MovingIconWithTrailingLineActivity.kt index 33858489ec..900f5ccf6a 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/linesandpolygons/MovingIconWithTrailingLineActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/linesandpolygons/MovingIconWithTrailingLineActivity.kt @@ -20,6 +20,7 @@ import com.mapbox.geojson.FeatureCollection import com.mapbox.geojson.LineString import com.mapbox.geojson.Point import com.mapbox.maps.EdgeInsets +import com.mapbox.maps.MapboxDelicateApi import com.mapbox.maps.Style import com.mapbox.maps.coroutine.awaitCameraForCoordinates import com.mapbox.maps.coroutine.awaitStyle @@ -38,6 +39,7 @@ import com.mapbox.maps.plugin.animation.MapAnimationOptions.Companion.mapAnimati import com.mapbox.maps.plugin.animation.easeTo import com.mapbox.maps.testapp.R import com.mapbox.maps.testapp.databinding.ActivityDdsMovingIconWithTrailingLineBinding +import com.mapbox.maps.toMapboxImage import com.mapbox.turf.TurfMeasurement import kotlinx.coroutines.launch import retrofit2.Call @@ -80,8 +82,8 @@ class MovingIconWithTrailingLineActivity : AppCompatActivity() { * @param featureCollection returned GeoJSON FeatureCollection from the Directions API route request */ private fun initData(style: Style, featureCollection: FeatureCollection) { - featureCollection.features()?.let { - (it[0].geometry() as? LineString)?.let { lineString -> + featureCollection.features()?.firstOrNull()?.geometry()?.let { + (it as? LineString)?.let { lineString -> routeCoordinateList = lineString.coordinates() initSources(style, featureCollection) initSymbolLayer(style) @@ -219,7 +221,9 @@ class MovingIconWithTrailingLineActivity : AppCompatActivity() { * Add the marker icon SymbolLayer. */ private fun initSymbolLayer(style: Style) { - style.addImage(MARKER_ID, BitmapFactory.decodeResource(resources, R.drawable.pink_dot)) + @OptIn(MapboxDelicateApi::class) + val image = BitmapFactory.decodeResource(resources, R.drawable.pink_dot).toMapboxImage() + style.addImage(MARKER_ID, image) style.addLayer( symbolLayer(SYMBOL_LAYER_ID, DOT_SOURCE_ID) { iconImage(MARKER_ID) diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/PointAnnotationActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/PointAnnotationActivity.kt index 5a88877b55..754d9d98e8 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/PointAnnotationActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/PointAnnotationActivity.kt @@ -160,62 +160,39 @@ class PointAnnotationActivity : AppCompatActivity() { } }) - val airplaneBitmap = bitmapFromDrawableRes( - this@PointAnnotationActivity, - R.drawable.ic_airplanemode_active_black_24dp - ) - airplaneBitmap?.let { - // create a symbol - val pointAnnotationOptions: PointAnnotationOptions = PointAnnotationOptions() - .withPoint(Point.fromLngLat(AIRPORT_LONGITUDE, AIRPORT_LATITUDE)) - .withIconImage(it) - .withTextField(ID_ICON_AIRPORT) - .withTextOffset(listOf(0.0, -2.0)) - .withTextColor(Color.RED) - .withIconSize(1.3) - .withIconOffset(listOf(0.0, -5.0)) - .withSymbolSortKey(10.0) - .withDraggable(true) - pointAnnotation = create(pointAnnotationOptions) + val airplaneBitmap = bitmapFromDrawableRes(R.drawable.ic_airplanemode_active_black_24dp) + // create a symbol + val pointAnnotationOptions: PointAnnotationOptions = PointAnnotationOptions() + .withPoint(Point.fromLngLat(AIRPORT_LONGITUDE, AIRPORT_LATITUDE)) + .withIconImage(airplaneBitmap) + .withTextField(ID_ICON_AIRPORT) + .withTextOffset(listOf(0.0, -2.0)) + .withTextColor(Color.RED) + .withIconSize(1.3) + .withIconOffset(listOf(0.0, -5.0)) + .withSymbolSortKey(10.0) + .withDraggable(true) + pointAnnotation = create(pointAnnotationOptions) - // random add symbols across the globe - val pointAnnotationOptionsList: MutableList = ArrayList() - for (i in 0..5) { - pointAnnotationOptionsList.add( - PointAnnotationOptions() - .withPoint(AnnotationUtils.createRandomPoint()) - .withIconImage(it) - .withDraggable(true) - ) - } - create(pointAnnotationOptionsList) - } + blueBitmap = bitmapFromDrawableRes(R.drawable.mapbox_user_icon) + // create nearby symbols + val nearbyOptions: PointAnnotationOptions = PointAnnotationOptions() + .withPoint(Point.fromLngLat(NEARBY_LONGITUDE, NEARBY_LATITUDE)) + .withIconImage(blueBitmap) + .withIconSize(2.5) + .withTextField(ID_ICON_AIRPORT) + .withSymbolSortKey(5.0) + .withDraggable(true) + create(nearbyOptions) - bitmapFromDrawableRes( - this@PointAnnotationActivity, - R.drawable.mapbox_user_icon - )?.let { - blueBitmap = it - // create nearby symbols - val nearbyOptions: PointAnnotationOptions = PointAnnotationOptions() - .withPoint(Point.fromLngLat(NEARBY_LONGITUDE, NEARBY_LATITUDE)) - .withIconImage(it) - .withIconSize(2.5) - .withTextField(ID_ICON_AIRPORT) - .withSymbolSortKey(5.0) - .withDraggable(true) - create(nearbyOptions) - } // random add symbols across the globe - airplaneBitmap?.let { - val pointAnnotationOptionsList = List(20) { - PointAnnotationOptions() - .withPoint(AnnotationUtils.createRandomPoint()) - .withIconImage(airplaneBitmap) - .withDraggable(true) - } - create(pointAnnotationOptionsList) + val pointAnnotationOptionsList = List(25) { + PointAnnotationOptions() + .withPoint(AnnotationUtils.createRandomPoint()) + .withIconImage(airplaneBitmap) + .withDraggable(true) } + create(pointAnnotationOptionsList) lifecycleScope.launch { val featureCollection = withContext(Dispatchers.Default) { FeatureCollection.fromJson( diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/infowindow/InfoWindowActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/infowindow/InfoWindowActivity.kt index e7337dda11..3dd81a3180 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/infowindow/InfoWindowActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/infowindow/InfoWindowActivity.kt @@ -6,12 +6,11 @@ import androidx.appcompat.app.AppCompatActivity import com.mapbox.geojson.Point import com.mapbox.maps.MapView import com.mapbox.maps.dsl.cameraOptions -import com.mapbox.maps.plugin.annotation.generated.* import com.mapbox.maps.plugin.gestures.OnMapLongClickListener import com.mapbox.maps.plugin.gestures.addOnMapLongClickListener import com.mapbox.maps.plugin.gestures.removeOnMapLongClickListener import com.mapbox.maps.testapp.R -import com.mapbox.maps.testapp.utils.BitmapUtils +import com.mapbox.maps.testapp.utils.BitmapUtils.bitmapFromDrawableRes import java.text.DecimalFormat /** @@ -33,10 +32,7 @@ class InfoWindowActivity : AppCompatActivity(), OnMapLongClickListener { mapView = MapView(this) setContentView(mapView) - icon = BitmapUtils.bitmapFromDrawableRes( - this@InfoWindowActivity, - R.drawable.ic_blue_marker - )!! + icon = bitmapFromDrawableRes(R.drawable.ic_blue_marker) mapView.mapboxMap.apply { setCamera( cameraOptions { diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/viewannotation/DynamicViewAnnotationActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/viewannotation/DynamicViewAnnotationActivity.kt index d6078dcafe..cf9380ed9e 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/viewannotation/DynamicViewAnnotationActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/viewannotation/DynamicViewAnnotationActivity.kt @@ -495,7 +495,7 @@ class DynamicViewAnnotationActivity : AppCompatActivity() { return BitmapDrawable( resources, BitmapUtils.drawableToBitmap( - getDrawable(this, R.drawable.bg_dva_eta), + getDrawable(this, R.drawable.bg_dva_eta)!!, flipX = flipX, flipY = flipY, tint = tint, diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/viewannotation/ViewAnnotationWithPointAnnotationActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/viewannotation/ViewAnnotationWithPointAnnotationActivity.kt index 481a706e58..2e9d5724d8 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/viewannotation/ViewAnnotationWithPointAnnotationActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/markersandcallouts/viewannotation/ViewAnnotationWithPointAnnotationActivity.kt @@ -19,11 +19,15 @@ import com.mapbox.maps.extension.style.layers.properties.generated.IconAnchor import com.mapbox.maps.plugin.annotation.Annotation import com.mapbox.maps.plugin.annotation.AnnotationConfig import com.mapbox.maps.plugin.annotation.annotations -import com.mapbox.maps.plugin.annotation.generated.* +import com.mapbox.maps.plugin.annotation.generated.OnPointAnnotationDragListener +import com.mapbox.maps.plugin.annotation.generated.PointAnnotation +import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager +import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions +import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager import com.mapbox.maps.testapp.R import com.mapbox.maps.testapp.databinding.ActivityViewAnnotationShowcaseBinding import com.mapbox.maps.testapp.databinding.ItemCalloutViewBinding -import com.mapbox.maps.testapp.utils.BitmapUtils +import com.mapbox.maps.testapp.utils.BitmapUtils.bitmapFromDrawableRes import com.mapbox.maps.viewannotation.ViewAnnotationManager import com.mapbox.maps.viewannotation.ViewAnnotationUpdateMode import com.mapbox.maps.viewannotation.annotationAnchor @@ -47,17 +51,12 @@ class ViewAnnotationWithPointAnnotationActivity : AppCompatActivity() { binding = ActivityViewAnnotationShowcaseBinding.inflate(layoutInflater) setContentView(binding.root) - val iconBitmap = BitmapUtils.bitmapFromDrawableRes( - this@ViewAnnotationWithPointAnnotationActivity, - R.drawable.ic_blue_marker - )!! - viewAnnotationManager = binding.mapView.viewAnnotationManager resetCamera() binding.mapView.mapboxMap.loadStyle(Style.STANDARD) { - prepareAnnotationMarker(binding.mapView, iconBitmap) + prepareAnnotationMarker(binding.mapView, bitmapFromDrawableRes(R.drawable.ic_blue_marker)) prepareViewAnnotation() // show / hide view annotation based on a marker click pointAnnotationManager.addClickListener { clickedAnnotation -> @@ -69,7 +68,7 @@ class ViewAnnotationWithPointAnnotationActivity : AppCompatActivity() { // show / hide view annotation based on marker visibility binding.fabStyleToggle.setOnClickListener { if (pointAnnotation.iconImage == null) { - pointAnnotation.iconImageBitmap = iconBitmap + pointAnnotation.iconImageBitmap = bitmapFromDrawableRes(R.drawable.ic_blue_marker) viewAnnotation.isVisible = true } else { pointAnnotation.iconImageBitmap = null diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/style/CustomRasterSourceActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/style/CustomRasterSourceActivity.kt index 9979607b4e..0c7f0809ec 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/style/CustomRasterSourceActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/style/CustomRasterSourceActivity.kt @@ -5,12 +5,12 @@ import android.graphics.Canvas import android.graphics.Color import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope import com.mapbox.geojson.Point import com.mapbox.maps.CanonicalTileID import com.mapbox.maps.CustomRasterSourceClient import com.mapbox.maps.CustomRasterSourceTileData import com.mapbox.maps.CustomRasterSourceTileStatus +import com.mapbox.maps.MapboxDelicateApi import com.mapbox.maps.MapboxExperimental import com.mapbox.maps.Style import com.mapbox.maps.dsl.cameraOptions @@ -22,9 +22,6 @@ import com.mapbox.maps.logI import com.mapbox.maps.plugin.gestures.gestures import com.mapbox.maps.testapp.databinding.ActivityCustomRasterSourceBinding import com.mapbox.maps.toMapboxImage -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch /** * Example of using custom raster source. @@ -86,16 +83,13 @@ class CustomRasterSourceActivity : AppCompatActivity() { } private fun refresh(requiredTiles: List) { - lifecycleScope.launch { - RasterTileProvider.createTiles( - requiredTiles, - nextColor, - TILE_SIZE, - TILE_SIZE - ) { tiles -> - customRasterSource.setTileData(tiles) - } - } + val tiles = RasterTileProvider.createTiles( + requiredTiles, + nextColor, + TILE_SIZE, + TILE_SIZE + ) + customRasterSource.setTileData(tiles) } companion object { @@ -127,22 +121,16 @@ class CustomRasterSourceActivity : AppCompatActivity() { * Utility to provide raster tiles. */ object RasterTileProvider { - suspend fun createTiles( + @OptIn(MapboxDelicateApi::class) + fun createTiles( requiredTiles: List, color: Int, width: Int, height: Int, - callback: (List) -> Unit - ) { + ): List { val image = createBitmap(color, width, height).toMapboxImage() - coroutineScope { - async { - requiredTiles.map { - CustomRasterSourceTileData(it, image) - } - }.await().also { - callback(it) - } + return requiredTiles.map { + CustomRasterSourceTileData(it, image) } } diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/terrain3D/SantaCatalinaActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/terrain3D/SantaCatalinaActivity.kt index 47476c91bb..e7a75b2648 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/terrain3D/SantaCatalinaActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/terrain3D/SantaCatalinaActivity.kt @@ -85,17 +85,11 @@ class SantaCatalinaActivity : AppCompatActivity() { } +image( FOREGROUND_ICON, - bitmapFromDrawableRes( - this@SantaCatalinaActivity, - R.drawable.mapbox_mylocation_icon_default - )!! + bitmapFromDrawableRes(R.drawable.mapbox_mylocation_icon_default) ) +image( BACKGROUND_ICON, - bitmapFromDrawableRes( - this@SantaCatalinaActivity, - R.drawable.mapbox_mylocation_bg_shape - )!! + bitmapFromDrawableRes(R.drawable.mapbox_mylocation_bg_shape) ) } ) { style -> diff --git a/app/src/main/java/com/mapbox/maps/testapp/utils/BitmapUtils.kt b/app/src/main/java/com/mapbox/maps/testapp/utils/BitmapUtils.kt index d90ae998d3..3daf1fa7da 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/utils/BitmapUtils.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/utils/BitmapUtils.kt @@ -17,23 +17,20 @@ object BitmapUtils { /** * Convert given drawable id to bitmap. */ - fun bitmapFromDrawableRes(context: Context, @DrawableRes resourceId: Int) = - drawableToBitmap(AppCompatResources.getDrawable(context, resourceId)) + fun Context.bitmapFromDrawableRes(@DrawableRes resourceId: Int): Bitmap = + drawableToBitmap(AppCompatResources.getDrawable(this, resourceId)!!) fun drawableToBitmap( - sourceDrawable: Drawable?, + sourceDrawable: Drawable, flipX: Boolean = false, flipY: Boolean = false, @ColorInt tint: Int? = null, - ): Bitmap? { - if (sourceDrawable == null) { - return null - } + ): Bitmap { return if (sourceDrawable is BitmapDrawable) { sourceDrawable.bitmap } else { // copying drawable object to not manipulate on the same reference - val constantState = sourceDrawable.constantState ?: return null + val constantState = sourceDrawable.constantState!! val drawable = constantState.newDrawable().mutate() val bitmap = Bitmap.createBitmap( drawable.intrinsicWidth, drawable.intrinsicHeight, diff --git a/compose-app/src/main/java/com/mapbox/maps/compose/testapp/examples/style/AnimatedImageSourceActivity.kt b/compose-app/src/main/java/com/mapbox/maps/compose/testapp/examples/style/AnimatedImageSourceActivity.kt index abb8f66f66..6480d8af92 100644 --- a/compose-app/src/main/java/com/mapbox/maps/compose/testapp/examples/style/AnimatedImageSourceActivity.kt +++ b/compose-app/src/main/java/com/mapbox/maps/compose/testapp/examples/style/AnimatedImageSourceActivity.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.res.imageResource import com.mapbox.geojson.Point +import com.mapbox.maps.MapboxDelicateApi import com.mapbox.maps.compose.testapp.ExampleScaffold import com.mapbox.maps.compose.testapp.R import com.mapbox.maps.compose.testapp.ui.theme.MapboxMapComposeTheme @@ -21,7 +22,9 @@ import com.mapbox.maps.extension.compose.style.sources.generated.rememberImageSo import com.mapbox.maps.extension.style.sources.generated.ImageSource import com.mapbox.maps.extension.style.sources.getSourceAs import com.mapbox.maps.extension.style.sources.updateImage +import com.mapbox.maps.toMapboxImage import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive /** * Load a raster image to a style using ImageSource and display it on a map as @@ -29,6 +32,7 @@ import kotlinx.coroutines.delay */ public class AnimatedImageSourceActivity : ComponentActivity() { + @OptIn(MapboxDelicateApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -44,19 +48,19 @@ public class AnimatedImageSourceActivity : ComponentActivity() { } } ) { - val bitmaps = listOf( - ImageBitmap.imageResource(R.drawable.southeast_radar_0).asAndroidBitmap(), - ImageBitmap.imageResource(R.drawable.southeast_radar_1).asAndroidBitmap(), - ImageBitmap.imageResource(R.drawable.southeast_radar_2).asAndroidBitmap(), - ImageBitmap.imageResource(R.drawable.southeast_radar_3).asAndroidBitmap(), + val images = listOf( + ImageBitmap.imageResource(R.drawable.southeast_radar_0).asAndroidBitmap().toMapboxImage(), + ImageBitmap.imageResource(R.drawable.southeast_radar_1).asAndroidBitmap().toMapboxImage(), + ImageBitmap.imageResource(R.drawable.southeast_radar_2).asAndroidBitmap().toMapboxImage(), + ImageBitmap.imageResource(R.drawable.southeast_radar_3).asAndroidBitmap().toMapboxImage(), ) MapEffect(Unit) { val imageSource: ImageSource = it.mapboxMap.getSourceAs(ID_IMAGE_SOURCE)!! var index = 0 - while (true) { - imageSource.updateImage(bitmaps[index]) - delay(1000) - index = (index + 1) % 4 + while (isActive) { + imageSource.updateImage(images[index++]) + index %= images.size + delay(1000L) } } RasterLayer( diff --git a/compose-app/src/main/java/com/mapbox/maps/compose/testapp/examples/style/ImageSourceActivity.kt b/compose-app/src/main/java/com/mapbox/maps/compose/testapp/examples/style/ImageSourceActivity.kt index 868dc85152..0ebd7e42ba 100644 --- a/compose-app/src/main/java/com/mapbox/maps/compose/testapp/examples/style/ImageSourceActivity.kt +++ b/compose-app/src/main/java/com/mapbox/maps/compose/testapp/examples/style/ImageSourceActivity.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.res.imageResource import com.mapbox.geojson.Point +import com.mapbox.maps.MapboxDelicateApi import com.mapbox.maps.Style import com.mapbox.maps.compose.testapp.ExampleScaffold import com.mapbox.maps.compose.testapp.R @@ -23,6 +24,7 @@ import com.mapbox.maps.extension.compose.style.sources.generated.rememberImageSo import com.mapbox.maps.extension.style.sources.generated.ImageSource import com.mapbox.maps.extension.style.sources.getSourceAs import com.mapbox.maps.extension.style.sources.updateImage +import com.mapbox.maps.toMapboxImage /** * Example to showcase usage of ImageSource. @@ -47,10 +49,10 @@ public class ImageSourceActivity : ComponentActivity() { MapStyle(style = Style.DARK) } ) { - val bitmap = ImageBitmap.imageResource(R.drawable.miami_beach).asAndroidBitmap() + @OptIn(MapboxDelicateApi::class) + val bitmap = ImageBitmap.imageResource(R.drawable.miami_beach).asAndroidBitmap().toMapboxImage() MapEffect(Unit) { - val imageSource: ImageSource = - it.mapboxMap.getSourceAs(ID_IMAGE_SOURCE)!! + val imageSource: ImageSource = it.mapboxMap.getSourceAs(ID_IMAGE_SOURCE)!! imageSource.updateImage(bitmap) } RasterLayer( diff --git a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/StyleImage.kt b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/StyleImage.kt index 74c8ef51bb..57f92f076e 100644 --- a/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/StyleImage.kt +++ b/extension-compose/src/main/java/com/mapbox/maps/extension/compose/style/StyleImage.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.unit.LayoutDirection import com.mapbox.maps.Image import com.mapbox.maps.ImageContent import com.mapbox.maps.ImageStretches +import com.mapbox.maps.MapboxDelicateApi import com.mapbox.maps.toMapboxImage /** @@ -52,6 +53,7 @@ public data class StyleImage( * * @return a [StyleImage] */ +@OptIn(MapboxDelicateApi::class) @Composable public fun rememberStyleImage( imageId: String, @@ -89,6 +91,7 @@ public fun rememberStyleImage( * * @return a [StyleImage] */ +@OptIn(MapboxDelicateApi::class) @Composable public fun rememberStyleImage( key: Any?, diff --git a/extension-style/src/main/java/com/mapbox/maps/extension/style/image/ImageExt.kt b/extension-style/src/main/java/com/mapbox/maps/extension/style/image/ImageExt.kt index 92496eff49..dc7fd81dc8 100644 --- a/extension-style/src/main/java/com/mapbox/maps/extension/style/image/ImageExt.kt +++ b/extension-style/src/main/java/com/mapbox/maps/extension/style/image/ImageExt.kt @@ -20,6 +20,7 @@ import com.mapbox.maps.extension.style.StyleContract replaceWith = ReplaceWith("image(imageId, image, block)") ) fun image(imageId: String, block: ImageExtensionImpl.Builder.() -> Unit): ImageExtensionImpl = + @Suppress("DEPRECATION") ImageExtensionImpl.Builder(imageId).apply(block).build() /** diff --git a/extension-style/src/main/java/com/mapbox/maps/extension/style/image/ImageExtensionImpl.kt b/extension-style/src/main/java/com/mapbox/maps/extension/style/image/ImageExtensionImpl.kt index 37a3d62cf5..3f18ff9180 100644 --- a/extension-style/src/main/java/com/mapbox/maps/extension/style/image/ImageExtensionImpl.kt +++ b/extension-style/src/main/java/com/mapbox/maps/extension/style/image/ImageExtensionImpl.kt @@ -77,6 +77,7 @@ class ImageExtensionImpl(private val builder: Builder) : StyleContract.StyleImag * @param imageId the id the the image extension * @param bitmap the bitmap data of the image. */ + @OptIn(MapboxDelicateApi::class) constructor(imageId: String, bitmap: Bitmap) { this.imageId = imageId this.internalImage = bitmap.toMapboxImage() @@ -123,6 +124,7 @@ class ImageExtensionImpl(private val builder: Builder) : StyleContract.StyleImag /** * Set bitmap data of the image. */ + @OptIn(MapboxDelicateApi::class) @Suppress("DeprecatedCallableAddReplaceWith") @Deprecated("Configuring image through `bitmap` function is deprecated, pass image to the `Builder(imageId: String, bitmap: Bitmap)` constructor instead.") fun bitmap(bitmap: Bitmap): Builder = apply { diff --git a/extension-style/src/main/java/com/mapbox/maps/extension/style/image/NinePatchUtils.kt b/extension-style/src/main/java/com/mapbox/maps/extension/style/image/NinePatchUtils.kt index c48112cca8..0632bf3d60 100644 --- a/extension-style/src/main/java/com/mapbox/maps/extension/style/image/NinePatchUtils.kt +++ b/extension-style/src/main/java/com/mapbox/maps/extension/style/image/NinePatchUtils.kt @@ -6,6 +6,7 @@ import com.mapbox.bindgen.Expected import com.mapbox.bindgen.None import com.mapbox.maps.ImageContent import com.mapbox.maps.ImageStretches +import com.mapbox.maps.MapboxDelicateApi import com.mapbox.maps.MapboxStyleManager import com.mapbox.maps.toMapboxImage import java.nio.ByteBuffer @@ -15,11 +16,14 @@ import java.nio.ByteOrder * Adds an 9-patch image to be used in the style. * X-stretches, Y-stretches and padding will be calculated from [Bitmap.getNinePatchChunk]. * + * Important: This method will allocate native memory outside the JVM heap on every call. + * * @param imageId ID of the image. * @param bitmap Bitmap data of the image. * * @return A string describing an error if the operation was not successful, empty otherwise. */ +@OptIn(MapboxDelicateApi::class) @JvmOverloads fun MapboxStyleManager.addImage9Patch( imageId: String, @@ -43,9 +47,13 @@ fun MapboxStyleManager.addImage9Patch( * Utility function returning [NinePatchImage] from a given [Bitmap]. * * [Bitmap] has to be in 9-patch format (.9.png) or [RuntimeException] will be thrown. + * + * Important: This method will allocate native memory outside the JVM heap on every call. */ +@OptIn(MapboxDelicateApi::class) fun Bitmap.parse9PatchBitmap() = decodeNinePatchChunk(this) +@MapboxDelicateApi private fun decodeNinePatchChunk(bitmap: Bitmap): NinePatchImage { val ninePatchChunk = bitmap.ninePatchChunk ?: throw IllegalArgumentException("Given bitmap must be a 9-patch drawable (.9.png)!") diff --git a/extension-style/src/main/java/com/mapbox/maps/extension/style/sources/ImageSourceExt.kt b/extension-style/src/main/java/com/mapbox/maps/extension/style/sources/ImageSourceExt.kt index ce305aab8b..2b1bd71c55 100644 --- a/extension-style/src/main/java/com/mapbox/maps/extension/style/sources/ImageSourceExt.kt +++ b/extension-style/src/main/java/com/mapbox/maps/extension/style/sources/ImageSourceExt.kt @@ -2,6 +2,7 @@ package com.mapbox.maps.extension.style.sources import android.graphics.Bitmap import com.mapbox.maps.Image +import com.mapbox.maps.MapboxDelicateApi import com.mapbox.maps.extension.style.sources.generated.ImageSource import com.mapbox.maps.extension.style.utils.check import com.mapbox.maps.toMapboxImage @@ -11,8 +12,12 @@ import com.mapbox.maps.toMapboxImage * * See [https://docs.mapbox.com/mapbox-gl-js/style-spec/#sources-image](https://docs.mapbox.com/mapbox-gl-js/style-spec/#sources-image) * + * Important: This method will allocate native memory outside the JVM heap every time it is called. + * Even if the same [bitmap] is passed. + * * @param bitmap [Bitmap] to update given image style source. */ +@OptIn(MapboxDelicateApi::class) fun ImageSource.updateImage(bitmap: Bitmap) { this.delegate?.updateStyleImageSourceImage(this.sourceId, bitmap.toMapboxImage()).check() } diff --git a/extension-style/src/test/java/com/mapbox/maps/extension/style/sources/CustomRasterSourceTest.kt b/extension-style/src/test/java/com/mapbox/maps/extension/style/sources/CustomRasterSourceTest.kt index 97aa6daa55..771e2da8e9 100644 --- a/extension-style/src/test/java/com/mapbox/maps/extension/style/sources/CustomRasterSourceTest.kt +++ b/extension-style/src/test/java/com/mapbox/maps/extension/style/sources/CustomRasterSourceTest.kt @@ -7,6 +7,7 @@ import com.mapbox.bindgen.None import com.mapbox.maps.CanonicalTileID import com.mapbox.maps.CustomRasterSourceTileData import com.mapbox.maps.Image +import com.mapbox.maps.MapboxDelicateApi import com.mapbox.maps.MapboxExperimental import com.mapbox.maps.MapboxStyleException import com.mapbox.maps.MapboxStyleManager @@ -68,6 +69,7 @@ class CustomRasterSourceTest { verify { style.setStyleCustomRasterSourceTileData("testId", tileData) } } + @OptIn(MapboxDelicateApi::class) @Test fun setTileDataBitmapTest() { val tileID: CanonicalTileID = mockk() diff --git a/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/AnnotationManagerImpl.kt b/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/AnnotationManagerImpl.kt index e77dbf8542..91e03058fa 100644 --- a/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/AnnotationManagerImpl.kt +++ b/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/AnnotationManagerImpl.kt @@ -638,19 +638,25 @@ internal constructor( // Add icons to style from PointAnnotation. private fun addIconToStyle(style: MapboxStyleManager, annotations: Collection) { // Add icon image bitmap from point annotation - annotations - .filter { it.getType() == AnnotationType.PointAnnotation } - .forEach { - val symbol = it as PointAnnotation - symbol.iconImage?.let { image -> - if (image.startsWith(PointAnnotation.ICON_DEFAULT_NAME_PREFIX)) { - // User set the bitmap icon, add the icon to style - symbol.iconImageBitmap?.let { bitmap -> - style.addImage(image(image, bitmap)) + annotations.forEach { annotation -> + (annotation as? PointAnnotation)?.let { symbol -> + symbol.iconImage?.let { imageId -> + if (imageId.startsWith(PointAnnotation.ICON_DEFAULT_NAME_PREFIX)) { + /* + * Basically if an image with the `imageId` already exists we don't add it again. The + * reason we can do that is because the `imageId` starts with `ICON_DEFAULT_NAME_PREFIX` + * which means it has unique ID per bitmap. + */ + if (!style.hasStyleImage(imageId)) { + // User set the bitmap icon, add the icon to style + symbol.iconImageBitmap?.let { mapboxImage -> + style.addImage(image(imageId, mapboxImage)) + } } } } } + } } // Extension function on the JsonObject that annotation defines. diff --git a/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotation.kt b/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotation.kt index 54b4e92b3a..122887e200 100644 --- a/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotation.kt +++ b/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotation.kt @@ -64,12 +64,15 @@ class PointAnnotation( */ set(value) { if (value != null) { - field = value - if (iconImage == null || iconImage!!.startsWith(ICON_DEFAULT_NAME_PREFIX)) { - // User does not set iconImage, update iconImage to this new bitmap - iconImage = ICON_DEFAULT_NAME_PREFIX + value.hashCode() + if (field != value) { + field = value + if (iconImage == null || iconImage!!.startsWith(ICON_DEFAULT_NAME_PREFIX)) { + // User does not set iconImage, update iconImage to this new bitmap + iconImage = ICON_DEFAULT_NAME_PREFIX + value.hashCode() + } } } else { + field = null jsonObject.remove(PointAnnotationOptions.PROPERTY_ICON_IMAGE) } } diff --git a/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotationManager.kt b/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotationManager.kt index dbb1d263d8..00e6afb34f 100644 --- a/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotationManager.kt +++ b/plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotationManager.kt @@ -330,11 +330,13 @@ class PointAnnotationManager( set(value) { field = value if (value != null) { - if (iconImage == null || iconImage!!.startsWith(ICON_DEFAULT_NAME_PREFIX)) { - // User does not set iconImage, update iconImage to this new bitmap - val imageId = ICON_DEFAULT_NAME_PREFIX + value.hashCode() - iconImage = imageId - addStyleImage(imageId, value) + if (field != value) { + if (iconImage == null || iconImage!!.startsWith(ICON_DEFAULT_NAME_PREFIX)) { + // User does not set iconImage, update iconImage to this new bitmap + val imageId = ICON_DEFAULT_NAME_PREFIX + value.hashCode() + iconImage = imageId + addStyleImage(imageId, value) + } } } else { iconImage = null diff --git a/plugin-annotation/src/test/java/com/mapbox/maps/plugin/annotation/generated/CircleAnnotationManagerTest.kt b/plugin-annotation/src/test/java/com/mapbox/maps/plugin/annotation/generated/CircleAnnotationManagerTest.kt index 426c56426e..6f79777ba7 100644 --- a/plugin-annotation/src/test/java/com/mapbox/maps/plugin/annotation/generated/CircleAnnotationManagerTest.kt +++ b/plugin-annotation/src/test/java/com/mapbox/maps/plugin/annotation/generated/CircleAnnotationManagerTest.kt @@ -175,6 +175,7 @@ class CircleAnnotationManagerTest { @Test fun createWithClusterOptions() { + @Suppress("UNCHECKED_CAST") manager = CircleAnnotationManager( delegateProvider, AnnotationConfig( @@ -583,6 +584,7 @@ class CircleAnnotationManagerTest { ) } returns mockk() + @Suppress("UNCHECKED_CAST") manager = CircleAnnotationManager( delegateProvider, AnnotationConfig( @@ -665,6 +667,7 @@ class CircleAnnotationManagerTest { ) } returns mockk() + @Suppress("UNCHECKED_CAST") manager = CircleAnnotationManager( delegateProvider, AnnotationConfig( diff --git a/plugin-annotation/src/test/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotationManagerTest.kt b/plugin-annotation/src/test/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotationManagerTest.kt index 729d6fa7c9..9da7ec90f3 100644 --- a/plugin-annotation/src/test/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotationManagerTest.kt +++ b/plugin-annotation/src/test/java/com/mapbox/maps/plugin/annotation/generated/PointAnnotationManagerTest.kt @@ -240,6 +240,7 @@ class PointAnnotationManagerTest { @Test fun createWithClusterOptions() { + @Suppress("UNCHECKED_CAST") manager = PointAnnotationManager( delegateProvider, AnnotationConfig( @@ -268,7 +269,7 @@ class PointAnnotationManagerTest { } @Test - fun iconImageBitmapWithIconImage() { + fun iconImageBitmapWithIconImageIdAndBitmap() { every { style.styleSourceExists(any()) } returns true every { style.styleLayerExists(any()) } returns true val annotation = manager.create( @@ -282,45 +283,81 @@ class PointAnnotationManagerTest { } @Test - fun iconImageBitmapWithoutIconImage() { + fun iconImageBitmapWithoutIconImageId() { every { style.styleSourceExists(any()) } returns true every { style.styleLayerExists(any()) } returns true mockkStatic(DataRef::class) every { DataRef.allocateNative(any()) } returns mockk(relaxed = true) + val imageId = PointAnnotation.ICON_DEFAULT_NAME_PREFIX + bitmap.hashCode() + every { style.hasStyleImage(imageId) } returns false val annotation = manager.create( PointAnnotationOptions() .withIconImage(bitmap) .withPoint(Point.fromLngLat(0.0, 0.0)) ) - assertEquals(PointAnnotation.ICON_DEFAULT_NAME_PREFIX + bitmap.hashCode(), annotation.iconImage) + assertEquals(imageId, annotation.iconImage) verify(exactly = 1) { style.addStyleImage(any(), any(), any(), any(), any(), any(), any()) } unmockkStatic(DataRef::class) } @Test - fun iconImageBitmapWithSameImage() { + fun twoPointsWithSameImageBitmap() { every { style.styleSourceExists(any()) } returns true every { style.styleLayerExists(any()) } returns true mockkStatic(DataRef::class) every { DataRef.allocateNative(any()) } returns mockk(relaxed = true) + val imageId = PointAnnotation.ICON_DEFAULT_NAME_PREFIX + bitmap.hashCode() + every { style.hasStyleImage(imageId) } returns false + val annotation = manager.create( PointAnnotationOptions() .withIconImage(bitmap) .withPoint(Point.fromLngLat(0.0, 0.0)) ) - assertEquals(PointAnnotation.ICON_DEFAULT_NAME_PREFIX + bitmap.hashCode(), annotation.iconImage) + assertEquals(imageId, annotation.iconImage) - verify(exactly = 1) { style.addStyleImage(PointAnnotation.ICON_DEFAULT_NAME_PREFIX + bitmap.hashCode(), any(), any(), any(), any(), any(), any()) } + verify(exactly = 1) { style.addStyleImage(imageId, any(), any(), any(), any(), any(), any()) } + every { style.hasStyleImage(imageId) } returns true val annotation2 = manager.create( PointAnnotationOptions() .withIconImage(bitmap) .withPoint(Point.fromLngLat(0.0, 0.0)) ) - assertEquals(PointAnnotation.ICON_DEFAULT_NAME_PREFIX + bitmap.hashCode(), annotation2.iconImage) - // The first one will trigger twice and the second one once. - verify(exactly = 3) { style.addStyleImage(PointAnnotation.ICON_DEFAULT_NAME_PREFIX + bitmap.hashCode(), any(), any(), any(), any(), any(), any()) } + assertEquals(imageId, annotation2.iconImage) + // Only one image should be added to the style because we're re-using the same bitmap + verify(exactly = 1) { style.addStyleImage(imageId, any(), any(), any(), any(), any(), any()) } + unmockkStatic(DataRef::class) + } + + @Test + fun onePointUpdateImageBitmap() { + every { style.styleSourceExists(any()) } returns true + every { style.styleLayerExists(any()) } returns true + mockkStatic(DataRef::class) + every { DataRef.allocateNative(any()) } returns mockk(relaxed = true) + val imageId = PointAnnotation.ICON_DEFAULT_NAME_PREFIX + bitmap.hashCode() + every { style.hasStyleImage(imageId) } returns false + + val annotation = manager.create( + PointAnnotationOptions() + .withIconImage(bitmap) + .withPoint(Point.fromLngLat(0.0, 0.0)) + ) + assertEquals(imageId, annotation.iconImage) + + verify(exactly = 1) { style.addStyleImage(imageId, any(), any(), any(), any(), any(), any()) } + + every { style.hasStyleImage(imageId) } returns true + + val secondBitmap = Bitmap.createBitmap(40, 40, Bitmap.Config.ARGB_8888) + val secondImageId = PointAnnotation.ICON_DEFAULT_NAME_PREFIX + secondBitmap.hashCode() + every { style.hasStyleImage(secondImageId) } returns false + annotation.iconImageBitmap = secondBitmap + manager.update(annotation) + // Only one image should be added to the style because we're re-using the same bitmap + verify(exactly = 1) { style.addStyleImage(secondImageId, any(), any(), any(), any(), any(), any()) } unmockkStatic(DataRef::class) } @@ -330,17 +367,23 @@ class PointAnnotationManagerTest { every { style.styleLayerExists(any()) } returns true mockkStatic(DataRef::class) every { DataRef.allocateNative(any()) } returns mockk(relaxed = true) + val imageId = PointAnnotation.ICON_DEFAULT_NAME_PREFIX + bitmap.hashCode() + every { style.hasStyleImage(imageId) } returns false val annotation = manager.create( PointAnnotationOptions() .withIconImage(bitmap) .withPoint(Point.fromLngLat(0.0, 0.0)) ) - verify(exactly = 1) { style.addStyleImage(PointAnnotation.ICON_DEFAULT_NAME_PREFIX + bitmap.hashCode(), any(), any(), any(), any(), any(), any()) } + verify(exactly = 1) { style.addStyleImage(imageId, any(), any(), any(), any(), any(), any()) } + + // Update the bitmap for the same point annotation val createBitmap = Bitmap.createBitmap(40, 40, Bitmap.Config.ARGB_8888) + val secondImageId = PointAnnotation.ICON_DEFAULT_NAME_PREFIX + createBitmap.hashCode() + every { style.hasStyleImage(secondImageId) } returns false annotation.iconImageBitmap = createBitmap manager.update(annotation) - assertEquals(PointAnnotation.ICON_DEFAULT_NAME_PREFIX + createBitmap.hashCode(), annotation.iconImage) - verify(exactly = 1) { style.addStyleImage(PointAnnotation.ICON_DEFAULT_NAME_PREFIX + createBitmap.hashCode(), any(), any(), any(), any(), any(), any()) } + assertEquals(secondImageId, annotation.iconImage) + verify(exactly = 1) { style.addStyleImage(secondImageId, any(), any(), any(), any(), any(), any()) } unmockkStatic(DataRef::class) } @Test @@ -874,6 +917,7 @@ class PointAnnotationManagerTest { ) } returns mockk() + @Suppress("UNCHECKED_CAST") manager = PointAnnotationManager( delegateProvider, AnnotationConfig( @@ -956,6 +1000,7 @@ class PointAnnotationManagerTest { ) } returns mockk() + @Suppress("UNCHECKED_CAST") manager = PointAnnotationManager( delegateProvider, AnnotationConfig( diff --git a/sdk-base/api/Release/metalava.txt b/sdk-base/api/Release/metalava.txt index 0c87e02c14..e8faf84bdf 100644 --- a/sdk-base/api/Release/metalava.txt +++ b/sdk-base/api/Release/metalava.txt @@ -41,7 +41,7 @@ package com.mapbox.maps { public final class ExtensionUtils { method public static com.mapbox.maps.CameraOptions toCameraOptions(com.mapbox.maps.CameraState, com.mapbox.maps.ScreenCoordinate? anchor = null); method public static com.mapbox.maps.CameraOptions toCameraOptions(com.mapbox.maps.CameraState); - method public static com.mapbox.maps.Image toMapboxImage(android.graphics.Bitmap); + method @com.mapbox.maps.MapboxDelicateApi public static com.mapbox.maps.Image toMapboxImage(android.graphics.Bitmap); } @kotlinx.parcelize.Parcelize public final class ImageHolder implements android.os.Parcelable { @@ -98,7 +98,7 @@ package com.mapbox.maps { ctor public MapboxConcurrentGeometryModificationException(String exceptionText, String sourceId); } - @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING, message="This is a delicate API and its use requires care." + " Make sure you fully read and understand documentation of the declaration that is marked as a delicate API.") @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface MapboxDelicateApi { + @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING, message="This is a delicate API and its use requires care." + " Make sure you fully read and understand documentation of the declaration that is marked as a delicate API.") @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface MapboxDelicateApi { } @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING, message="This API is experimental. It may be changed in the future without notice.") @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface MapboxExperimental { diff --git a/sdk-base/src/main/java/com/mapbox/maps/ExtensionUtils.kt b/sdk-base/src/main/java/com/mapbox/maps/ExtensionUtils.kt index e1d28f8672..fe10dbab1d 100644 --- a/sdk-base/src/main/java/com/mapbox/maps/ExtensionUtils.kt +++ b/sdk-base/src/main/java/com/mapbox/maps/ExtensionUtils.kt @@ -7,7 +7,11 @@ import com.mapbox.bindgen.DataRef /** * Convert [Bitmap] to rendering engine [Image] instance. + * + * Important: This will allocate native memory outside the JVM heap. If possible, avoid using it + * inside loops/animations. */ +@MapboxDelicateApi fun Bitmap.toMapboxImage(): Image { if (config != Bitmap.Config.ARGB_8888) { throw IllegalArgumentException("Only ARGB_8888 bitmap config is supported!") diff --git a/sdk-base/src/main/java/com/mapbox/maps/MapboxDelicateApi.kt b/sdk-base/src/main/java/com/mapbox/maps/MapboxDelicateApi.kt index b64783740b..35299d4488 100644 --- a/sdk-base/src/main/java/com/mapbox/maps/MapboxDelicateApi.kt +++ b/sdk-base/src/main/java/com/mapbox/maps/MapboxDelicateApi.kt @@ -13,6 +13,6 @@ package com.mapbox.maps " Make sure you fully read and understand documentation of the declaration that is marked as a delicate API." ) @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.CONSTRUCTOR) @MustBeDocumented annotation class MapboxDelicateApi \ No newline at end of file diff --git a/sdk-base/src/main/java/com/mapbox/maps/MapboxStyleManager.kt b/sdk-base/src/main/java/com/mapbox/maps/MapboxStyleManager.kt index 1e53d63846..78cd9a2828 100644 --- a/sdk-base/src/main/java/com/mapbox/maps/MapboxStyleManager.kt +++ b/sdk-base/src/main/java/com/mapbox/maps/MapboxStyleManager.kt @@ -1395,6 +1395,7 @@ open class MapboxStyleManager @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) * * @return A string describing an error if the operation was not successful, empty otherwise. */ + @OptIn(MapboxDelicateApi::class) @CallSuper @MainThread fun addImage(