Enhance MapScreen with location permission handling and camera positioning
- Added functionality to request location permissions and navigate to the user's current location on the map. - Integrated camera state management to animate the map view to the user's location. - Updated UI to include a floating action button for accessing the user's location and refreshing zone data.
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
package com.geozoner.app.ui.map
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MyLocation
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -13,21 +21,32 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.geozoner.app.data.remote.dto.ZoneBriefResponse
|
||||
import com.geozoner.app.ui.theme.ZoneColors
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.location.Priority
|
||||
import com.google.android.gms.tasks.CancellationTokenSource
|
||||
import kotlinx.coroutines.launch
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.rememberCameraState
|
||||
import org.maplibre.compose.expressions.dsl.const
|
||||
import org.maplibre.compose.layers.FillLayer
|
||||
import org.maplibre.compose.layers.LineLayer
|
||||
import org.maplibre.compose.map.MaplibreMap
|
||||
import org.maplibre.compose.sources.GeoJsonData
|
||||
import org.maplibre.compose.sources.rememberGeoJsonSource
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private const val OPENFREEMAP_STYLE = "https://tiles.openfreemap.org/styles/liberty"
|
||||
|
||||
@@ -36,6 +55,31 @@ fun MapScreen(
|
||||
viewModel: MapViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val cameraState = rememberCameraState(
|
||||
firstPosition = CameraPosition(
|
||||
target = Position(latitude = 55.7558, longitude = 37.6173), // Moscow default
|
||||
zoom = 12.0,
|
||||
),
|
||||
)
|
||||
|
||||
var hasLocationPermission by remember {
|
||||
mutableStateOf(
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
)
|
||||
}
|
||||
|
||||
val permissionLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { permissions ->
|
||||
hasLocationPermission = permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true
|
||||
if (hasLocationPermission) {
|
||||
goToMyLocation(context, scope, cameraState)
|
||||
}
|
||||
}
|
||||
|
||||
// Build GeoJSON strings for own and friend zones
|
||||
val myZoneGeoJson = remember(state.myZones) {
|
||||
@@ -50,6 +94,7 @@ fun MapScreen(
|
||||
MaplibreMap(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
baseStyle = org.maplibre.compose.style.BaseStyle.Uri(OPENFREEMAP_STYLE),
|
||||
cameraState = cameraState,
|
||||
) {
|
||||
// Friend zones layer (rendered below own zones)
|
||||
if (friendZoneGeoJson.isNotEmpty()) {
|
||||
@@ -115,22 +160,76 @@ fun MapScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Refresh button
|
||||
SmallFloatingActionButton(
|
||||
onClick = { viewModel.loadZones() },
|
||||
// FABs column (top-end)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = 16.dp, end = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
// My Location button
|
||||
SmallFloatingActionButton(
|
||||
onClick = {
|
||||
if (hasLocationPermission) {
|
||||
goToMyLocation(context, scope, cameraState)
|
||||
} else {
|
||||
permissionLauncher.launch(
|
||||
arrayOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
Icon(Icons.Default.MyLocation, contentDescription = "My location")
|
||||
}
|
||||
|
||||
// Refresh zones button
|
||||
SmallFloatingActionButton(
|
||||
onClick = { viewModel.loadZones() },
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Refresh zones")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current location and animates the camera to it.
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun goToMyLocation(
|
||||
context: android.content.Context,
|
||||
scope: kotlinx.coroutines.CoroutineScope,
|
||||
cameraState: org.maplibre.compose.camera.CameraState,
|
||||
) {
|
||||
val fusedClient = LocationServices.getFusedLocationProviderClient(context)
|
||||
val cancellationToken = CancellationTokenSource()
|
||||
|
||||
fusedClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, cancellationToken.token)
|
||||
.addOnSuccessListener { location ->
|
||||
if (location != null) {
|
||||
scope.launch {
|
||||
cameraState.animateTo(
|
||||
finalPosition = CameraPosition(
|
||||
target = Position(
|
||||
latitude = location.latitude,
|
||||
longitude = location.longitude,
|
||||
),
|
||||
zoom = 15.0,
|
||||
),
|
||||
duration = 1.seconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a GeoJSON FeatureCollection string from zone responses.
|
||||
* Each zone becomes a Feature with a Polygon geometry.
|
||||
*/
|
||||
private fun buildZoneGeoJson(zones: List<ZoneBriefResponse>): String {
|
||||
if (zones.isEmpty()) return ""
|
||||
|
||||
Reference in New Issue
Block a user