I’m working on an Android application using Kotlin, Jetpack Compose, and Osmdroid. The goal is to overlay an image onto a map, allowing users to interact with both the image and the map with specific gestures.
Here is what I want to achieve:
- The user can upload an image into a Compose Image object.
- The image can be moved and scaled with gestures, but it should not be rotatable.
- The map can be zoomed in and out, and rotated by the user.
- The image should remain correctly positioned on the map during map movements and scaling.
I have successfully managed to move and scale the image on the map, and add the image as a GroundOverlay on the map when only zooming and panning are involved. However, I am encountering an issue with maintaining the correct position of the GroundOverlay when the map is rotated. The overlay shifts and no longer aligns correctly with the image in the Compose Image.
Could someone provide guidance or an example on how to synchronize the position of a GroundOverlay with a movable and scalable image in Jetpack Compose when the map is rotated?
Here’s a summary of the issue:
- Zooming and panning work fine for both the map and the image.
- Map rotation causes the GroundOverlay to shift, misaligning it with the image.
Image of correct overlay without rotating the map:
enter image description here
Image when I’m rotating map:
enter image description here
I already tried this
Here is the code of moving the image
@Composable
fun DraggableAndScalableImage(
imagePainter: AsyncImagePainter,
layerViewModel: LayerViewModel,
offsetX: MutableState<Float>,
offsetY: MutableState<Float>,
scale: MutableState<Float>,
rotation: MutableState<Float>,
boxSize: MutableState<DpSize>,
) {
val transparencyValue = layerViewModel.transparencyValue
Image(
painter = imagePainter,
contentDescription = null,
modifier = Modifier
.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
.graphicsLayer(
scaleX = scale.value,
scaleY = scale.value,
rotationZ = rotation.value,
transformOrigin = TransformOrigin(0f,0f)
)
.size(boxSize.value.width, boxSize.value.height)
.background(Color.Transparent)
.pointerInput(Unit) {
detectTransformGestures { _, pan, gestureZoom, _ ->
if (gestureZoom != 1f) {
val newScale = scale.value * gestureZoom
val focusX = pan.x
val focusY = pan.y
offsetX.value =
((offsetX.value - focusX) * (newScale / scale.value)) + focusX
offsetY.value =
((offsetY.value - focusY) * (newScale / scale.value)) + focusY
scale.value = newScale
}
offsetX.value += pan.x
offsetY.value += pan.y
}
},
alpha = 1 - transparencyValue.value,
)
}
here is the code of getting Accurate position of image on the map
fun getAccurateGeoPoints(
map: MapView,
offsetX: MutableState<Float>,
offsetY: MutableState<Float>,
scale: MutableState<Float>,
boxSize: DpSize,
density: Float,
): List<GeoPoint> {
val leftMap = map.projection.screenRect.left
val topMap = map.projection.screenRect.top
val projection = map.projection
// Calculate scaled box dimensions
val scaledBoxWidth = density * boxSize.width.value * scale.value
val scaledBoxHeight = density * boxSize.height.value * scale.value
// Calculate the center of the scaled box in screen coordinates
val scaledBoxCenterX = leftMap + offsetX.value + scaledBoxWidth / 2
val scaledBoxCenterY = topMap + offsetY.value + scaledBoxHeight / 2
val scaledBoxCenter = PointF(scaledBoxCenterX, scaledBoxCenterY)
// Rotation calculations (adjusting for map rotation)
val mapRotationInRadians = Math.toRadians(map.mapOrientation.toDouble())
val angleInRadians = mapRotationInRadians
val cosAngle = Math.cos(angleInRadians)
val sinAngle = Math.sin(angleInRadians)
val mapCenterGeoPoint = map.mapCenter
val mapCenterPoint = PointF(mapCenterGeoPoint.longitude.toFloat(), mapCenterGeoPoint.latitude.toFloat())
// Calculate rotated corner points around the scaled center
val boxTopLeft = calculateRotatedPoint(scaledBoxCenter, -scaledBoxWidth / 2, -scaledBoxHeight / 2, cosAngle, sinAngle)
val boxTopRight = calculateRotatedPoint(scaledBoxCenter, scaledBoxWidth / 2, -scaledBoxHeight / 2, cosAngle, sinAngle)
val boxBottomLeft = calculateRotatedPoint(scaledBoxCenter, -scaledBoxWidth / 2, scaledBoxHeight / 2, cosAngle, sinAngle)
val boxBottomRight = calculateRotatedPoint(scaledBoxCenter, scaledBoxWidth / 2, scaledBoxHeight / 2, cosAngle, sinAngle)
// Project corner points to GeoPoints
val geoBoxTopLeft = GeoPoint(projection.fromPixels(boxTopLeft.x.toInt(), boxTopLeft.y.toInt()))
val geoBoxTopRight = GeoPoint(projection.fromPixels(boxTopRight.x.toInt(), boxTopRight.y.toInt()))
val geoBoxBottomLeft = GeoPoint(projection.fromPixels(boxBottomLeft.x.toInt(), boxBottomLeft.y.toInt()))
val geoBoxBottomRight = GeoPoint(projection.fromPixels(boxBottomRight.x.toInt(), boxBottomRight.y.toInt()))
return listOf(geoBoxTopLeft, geoBoxTopRight, geoBoxBottomLeft, geoBoxBottomRight)
}
private fun calculateRotatedPoint(origin: PointF, offsetX: Float, offsetY: Float, cosAngle: Double, sinAngle: Double): PointD {
val rotatedX = (offsetX * cosAngle + offsetY * sinAngle) + origin.x
val rotatedY = (-offsetX * sinAngle + offsetY * cosAngle) + origin.y
return PointD(rotatedX, rotatedY)
}
data class PointD(val x: Double, val y: Double)
and the code of overlay:
fun createImageOverlay(
map: MapView,
bitmap: MutableState<Bitmap?>,
accurateGeoPoints: List<GeoPoint>,
layerViewModel: LayerViewModel,
) {
val overlays = map.overlays
var groundOverlay: GroundOverlay? = null
bitmap.value?.let {
for (overlay in overlays) {
if (overlay is GroundOverlay && overlay.image == bitmap.value) {
groundOverlay = overlay
break
}
}
if (groundOverlay == null) {
groundOverlay = GroundOverlay() .apply {
image = bitmap.value
transparency = layerViewModel.transparencyValue.value
setPositionFromBounds(accurateGeoPoints[0], accurateGeoPoints[1], accurateGeoPoints[3], accurateGeoPoints[2])
}
map.overlays.add(groundOverlay)
val overlays = map.overlays
overlays.sortWith(compareBy { overlay ->
when (overlay) {
is GroundOverlay -> 1
is MyLocationNewOverlay -> 3
else -> 2
}
})
}
}
map.invalidate()
}
Thank you in advance for your help!
okon is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.