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