diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_zoom_in.xml b/composeApp/src/commonMain/composeResources/drawable/ic_zoom_in.xml
new file mode 100644
index 0000000..dd47c7d
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_zoom_in.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_zoom_out.xml b/composeApp/src/commonMain/composeResources/drawable/ic_zoom_out.xml
new file mode 100644
index 0000000..6d68d68
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/ic_zoom_out.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/composeApp/src/commonMain/kotlin/com/farimarwat/krossmapdemo/App.kt b/composeApp/src/commonMain/kotlin/com/farimarwat/krossmapdemo/App.kt
index 4edab65..6a4dcbb 100644
--- a/composeApp/src/commonMain/kotlin/com/farimarwat/krossmapdemo/App.kt
+++ b/composeApp/src/commonMain/kotlin/com/farimarwat/krossmapdemo/App.kt
@@ -39,6 +39,8 @@ import krossmapdemo.composeapp.generated.resources.ic_3d_view_cross
import krossmapdemo.composeapp.generated.resources.ic_current_location
import krossmapdemo.composeapp.generated.resources.ic_direction
import krossmapdemo.composeapp.generated.resources.ic_direction_cross
+import krossmapdemo.composeapp.generated.resources.ic_zoom_in
+import krossmapdemo.composeapp.generated.resources.ic_zoom_out
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview
@@ -49,7 +51,7 @@ fun App() {
MaterialTheme {
val latitude = 32.60370
val longitude = 70.92179
- val zoom = 17f
+ var zoomValue by remember { mutableStateOf(17f) }
val currentLocationMarker = remember {
mutableStateOf(null)
}
@@ -73,26 +75,8 @@ fun App() {
BindEffect(permissionController)
LaunchedEffect(Unit) {
permissionGranted = permissionController.isPermissionGranted(Permission.LOCATION)
- if (!permissionGranted) {
- try {
- permissionController.providePermission(Permission.LOCATION)
- permissionGranted =
- permissionController.isPermissionGranted(Permission.LOCATION)
- } catch (ex: DeniedException) {
- permissionController.openAppSettings()
- println(ex)
- } catch (ex: DeniedAlwaysException) {
- permissionController.openAppSettings()
- println(ex)
- } catch (ex: RequestCanceledException) {
- println(ex)
- }
- } else {
- println("Permission already granted")
- }
}
-
//Create Map State
val mapState = rememberKrossMapState()
var cameraFollow by remember { mutableStateOf(true)}
@@ -100,7 +84,7 @@ fun App() {
val cameraState = rememberKrossCameraPositionState(
latitude = latitude,
longitude = longitude,
- zoom = zoom,
+ zoom = zoomValue,
cameraFollow = cameraFollow
)
@@ -112,17 +96,18 @@ fun App() {
icon = Res.readBytes("drawable/ic_tracker.png")
)
}
+
LaunchedEffect(Unit) {
mapState.onUpdateLocation = {
currentLocationMarker.value = currentLocationMarker.value?.copy(coordinate = it)
currentLocationMarker.value?.let { cm ->
mapState.addOrUpdateMarker(cm)
}
-
}
}
- LaunchedEffect(navigation) {
- if (navigation) {
+
+ LaunchedEffect(navigation, permissionGranted) {
+ if (navigation && permissionGranted) {
mapState.startLocationUpdate()
} else {
mapState.stopLocationUpdate()
@@ -152,47 +137,75 @@ fun App() {
mapState.addPolyLine(polyline)
}
- if (permissionGranted) {
- //Create Map
- KrossMap(
- modifier = Modifier.fillMaxSize(),
- mapState = mapState,
- cameraPositionState = cameraState,
- mapSettings = {
- MapSettings(
- tilt = cameraState.tilt,
- navigation = navigation,
- onCurrentLocationClicked = {
- mapState.requestCurrentLocation()
- scope.launch {
- mapState.currentLocation?.let{
- cameraState.animateCamera(it.latitude, it.longitude)
- }
- }
- },
- toggle3DViewClicked = {
- scope.launch {
- cameraState.tilt = if (cameraState.tilt > 0) {
- 0f
- } else {
- 45f
+ //Create Map (always render; location permission requested only on demand)
+ KrossMap(
+ modifier = Modifier.fillMaxSize(),
+ mapState = mapState,
+ cameraPositionState = cameraState,
+ mapSettings = {
+ MapSettings(
+ tilt = cameraState.tilt,
+ navigation = navigation,
+ onCurrentLocationClicked = {
+ scope.launch {
+ var granted =
+ permissionController.isPermissionGranted(Permission.LOCATION)
+ if (!granted) {
+ try {
+ permissionController.providePermission(Permission.LOCATION)
+ } catch (ex: DeniedException) {
+ permissionController.openAppSettings()
+ println(ex)
+ } catch (ex: DeniedAlwaysException) {
+ permissionController.openAppSettings()
+ println(ex)
+ } catch (ex: RequestCanceledException) {
+ println(ex)
+ }
+ granted =
+ permissionController.isPermissionGranted(Permission.LOCATION)
+ }
+ permissionGranted = granted
+ if (granted) {
+ mapState.requestCurrentLocation()
+ mapState.currentLocation?.let { loc ->
+ cameraState.animateCamera(loc.latitude, loc.longitude)
}
-
- //Pause navigation for a second because it will effectly change the 3d mode.
- val oldNavigation = navigation
- navigation = false
- delay(500)
- cameraState.animateCamera(tilt = cameraState.tilt)
- navigation = oldNavigation
}
- },
- toggleNavigation = {
- navigation = !navigation
}
- )
- }
- )
- }
+ },
+ toggle3DViewClicked = {
+ scope.launch {
+ cameraState.tilt = if (cameraState.tilt > 0) {
+ 0f
+ } else {
+ 45f
+ }
+ val oldNavigation = navigation
+ navigation = false
+ delay(500)
+ cameraState.animateCamera(tilt = cameraState.tilt)
+ navigation = oldNavigation
+ }
+ },
+ toggleNavigation = {
+ navigation = !navigation
+ },
+ onZoomInClicked = {
+ scope.launch {
+ zoomValue++
+ cameraState.animateCamera(zoom = zoomValue)
+ }
+ },
+ onZoomOutClicked = {
+ scope.launch {
+ zoomValue--
+ cameraState.animateCamera(zoom = zoomValue)
+ }
+ }
+ )
+ }
+ )
}
}
@@ -204,7 +217,9 @@ fun MapSettings(
navigation: Boolean,
onCurrentLocationClicked: () -> Unit = {},
toggle3DViewClicked: () -> Unit = {},
- toggleNavigation: () -> Unit = {}
+ toggleNavigation: () -> Unit = {},
+ onZoomInClicked: () -> Unit = {},
+ onZoomOutClicked: () -> Unit = {},
) {
Column(
modifier = Modifier.padding(16.dp),
@@ -267,6 +282,40 @@ fun MapSettings(
tint = Color.White
)
}
+ IconButton(
+ modifier = Modifier
+ .size(48.dp)
+ .clip(CircleShape)
+ .background(Color.Blue),
+ onClick = onZoomInClicked
+ ) {
+ Icon(
+ modifier = Modifier
+ .size(24.dp),
+ painter = painterResource(
+ Res.drawable.ic_zoom_in
+ ),
+ contentDescription = "Zoom In",
+ tint = Color.White
+ )
+ }
+ IconButton(
+ modifier = Modifier
+ .size(48.dp)
+ .clip(CircleShape)
+ .background(Color.Blue),
+ onClick = onZoomOutClicked
+ ) {
+ Icon(
+ modifier = Modifier
+ .size(24.dp),
+ painter = painterResource(
+ Res.drawable.ic_zoom_out
+ ),
+ contentDescription = "Current Location",
+ tint = Color.White
+ )
+ }
}
}
diff --git a/krossmap/src/androidMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.android.kt b/krossmap/src/androidMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.android.kt
index 3a2efb3..843628f 100644
--- a/krossmap/src/androidMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.android.kt
+++ b/krossmap/src/androidMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.android.kt
@@ -11,6 +11,7 @@ import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.CameraPositionState
import com.google.maps.android.compose.rememberCameraPositionState
+import com.farimarwat.krossmap.model.KrossCoordinate
actual class KrossCameraPositionState(
internal val googleCameraPositionState: CameraPositionState?
@@ -18,6 +19,17 @@ actual class KrossCameraPositionState(
actual var tilt by mutableStateOf(0f)
actual var cameraFollow by mutableStateOf(true)
+
+ actual val center: KrossCoordinate?
+ get() {
+ val position = googleCameraPositionState?.position ?: return null
+ val target = position.target
+ return KrossCoordinate(
+ latitude = target.latitude,
+ longitude = target.longitude,
+ bearing = position.bearing
+ )
+ }
actual suspend fun animateCamera(
latitude: Double?,
longitude: Double?,
diff --git a/krossmap/src/commonMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.kt b/krossmap/src/commonMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.kt
index 0a2c634..be582e2 100644
--- a/krossmap/src/commonMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.kt
+++ b/krossmap/src/commonMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.kt
@@ -1,6 +1,7 @@
package com.farimarwat.krossmap.core
import androidx.compose.runtime.Composable
+import com.farimarwat.krossmap.model.KrossCoordinate
/**
* A multiplatform representation of a map camera controller and its state.
@@ -25,6 +26,12 @@ expect class KrossCameraPositionState {
*/
var cameraFollow: Boolean
+ /**
+ * Current center of the visible map region (camera target), if available.
+ * Returns null if the underlying map view/state is not yet ready.
+ */
+ val center: KrossCoordinate?
+
/**
* Animates the camera to the specified location and orientation.
*
diff --git a/krossmap/src/iosMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.ios.kt b/krossmap/src/iosMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.ios.kt
index 8ef2148..25075e8 100644
--- a/krossmap/src/iosMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.ios.kt
+++ b/krossmap/src/iosMain/kotlin/com/farimarwat/krossmap/core/KrossCameraPositionState.ios.kt
@@ -26,10 +26,21 @@ actual class KrossCameraPositionState(
actual var tilt by mutableStateOf(0f)
private var mapView: MKMapView? = null
- private var baseDistance: Double = 10000.0
-
actual var cameraFollow by mutableStateOf(false)
+ actual val center: KrossCoordinate?
+ @OptIn(ExperimentalForeignApi::class)
+ get() {
+ val currentCamera = mapView?.camera ?: camera
+ val coord = currentCamera.centerCoordinate ?: return null
+ val (lat, lng) = coord.useContents { this.latitude to this.longitude }
+ return KrossCoordinate(
+ latitude = lat,
+ longitude = lng,
+ bearing = currentCamera.heading.toFloat()
+ )
+ }
+
@OptIn(ExperimentalForeignApi::class)
actual suspend fun animateCamera(
latitude: Double?,
@@ -45,8 +56,7 @@ actual class KrossCameraPositionState(
val coordinate = CLLocationCoordinate2DMake(latitude ?: lat, longitude ?: lng)
- // Always calculate distance from base distance, not current distance
- val distance = zoom?.let { zoomToDistanceFromBase(it) } ?: currentCamera.centerCoordinateDistance
+ val distance = zoom?.let { zoomToDistance(it) } ?: currentCamera.centerCoordinateDistance
val newCamera = MKMapCamera.cameraLookingAtCenterCoordinate(
centerCoordinate = coordinate,
@@ -58,18 +68,11 @@ actual class KrossCameraPositionState(
mapView?.setCamera(newCamera, animated = true)
}
- private fun zoomToDistanceFromBase(zoom: Float): Double {
- // Calculate distance based on base distance, not current distance
- // Adjust this formula based on your zoom scale
- return baseDistance / (2.0.pow(zoom.toDouble() - 10.0)) // Assuming zoom 10 = base distance
- }
-
@OptIn(ExperimentalForeignApi::class)
internal fun setMapView(map: MKMapView) {
mapView = map
mapView?.setCamera(camera)
- baseDistance = mapView?.camera?.centerCoordinateDistance ?: 15000.0
}
@@ -103,6 +106,7 @@ actual fun rememberKrossCameraPositionState(
}
return state
}
+
fun zoomToDistance(zoom: Float): Double {
val baseDistance = when {
zoom >= 20 -> 200.0 // was 170.0