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
|
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.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.MyLocation
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -13,21 +21,32 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
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.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.geozoner.app.data.remote.dto.ZoneBriefResponse
|
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.expressions.dsl.const
|
||||||
import org.maplibre.compose.layers.FillLayer
|
import org.maplibre.compose.layers.FillLayer
|
||||||
import org.maplibre.compose.layers.LineLayer
|
import org.maplibre.compose.layers.LineLayer
|
||||||
import org.maplibre.compose.map.MaplibreMap
|
import org.maplibre.compose.map.MaplibreMap
|
||||||
import org.maplibre.compose.sources.GeoJsonData
|
import org.maplibre.compose.sources.GeoJsonData
|
||||||
import org.maplibre.compose.sources.rememberGeoJsonSource
|
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"
|
private const val OPENFREEMAP_STYLE = "https://tiles.openfreemap.org/styles/liberty"
|
||||||
|
|
||||||
@@ -36,6 +55,31 @@ fun MapScreen(
|
|||||||
viewModel: MapViewModel = hiltViewModel(),
|
viewModel: MapViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsState()
|
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
|
// Build GeoJSON strings for own and friend zones
|
||||||
val myZoneGeoJson = remember(state.myZones) {
|
val myZoneGeoJson = remember(state.myZones) {
|
||||||
@@ -50,6 +94,7 @@ fun MapScreen(
|
|||||||
MaplibreMap(
|
MaplibreMap(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
baseStyle = org.maplibre.compose.style.BaseStyle.Uri(OPENFREEMAP_STYLE),
|
baseStyle = org.maplibre.compose.style.BaseStyle.Uri(OPENFREEMAP_STYLE),
|
||||||
|
cameraState = cameraState,
|
||||||
) {
|
) {
|
||||||
// Friend zones layer (rendered below own zones)
|
// Friend zones layer (rendered below own zones)
|
||||||
if (friendZoneGeoJson.isNotEmpty()) {
|
if (friendZoneGeoJson.isNotEmpty()) {
|
||||||
@@ -115,22 +160,76 @@ fun MapScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh button
|
// FABs column (top-end)
|
||||||
SmallFloatingActionButton(
|
Column(
|
||||||
onClick = { viewModel.loadZones() },
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(top = 16.dp, end = 16.dp),
|
.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,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Refresh, contentDescription = "Refresh zones")
|
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.
|
* Builds a GeoJSON FeatureCollection string from zone responses.
|
||||||
* Each zone becomes a Feature with a Polygon geometry.
|
|
||||||
*/
|
*/
|
||||||
private fun buildZoneGeoJson(zones: List<ZoneBriefResponse>): String {
|
private fun buildZoneGeoJson(zones: List<ZoneBriefResponse>): String {
|
||||||
if (zones.isEmpty()) return ""
|
if (zones.isEmpty()) return ""
|
||||||
|
|||||||
Reference in New Issue
Block a user