From 39876a5a79b41d896a154b51211dd76d86b03b77 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Minh Date: Tue, 4 Jun 2024 13:21:40 +0700 Subject: [PATCH] update ui: add distance --- .../core/util/CalculateDistance.kt | 29 +++++ .../data/mapper/ResponseMapper.kt | 1 + .../data/remote/ApiService.kt | 9 +- .../searchaddresslab/data/remote/dto/Item.kt | 2 +- .../data/repository/HereRepositoryImpl.kt | 2 +- .../searchaddresslab/domain/model/Item.kt | 1 + .../presentation/screen/home/HomeScreen.kt | 15 ++- .../screen/search/SearchScreen.kt | 79 +++++++++++- .../screen/search/SearchScreenViewModel.kt | 4 + .../screen/search/components/AddressItem.kt | 113 +++++++++++------- 10 files changed, 200 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/com/nqmgaming/searchaddresslab/core/util/CalculateDistance.kt diff --git a/app/src/main/java/com/nqmgaming/searchaddresslab/core/util/CalculateDistance.kt b/app/src/main/java/com/nqmgaming/searchaddresslab/core/util/CalculateDistance.kt new file mode 100644 index 0000000..72b45e3 --- /dev/null +++ b/app/src/main/java/com/nqmgaming/searchaddresslab/core/util/CalculateDistance.kt @@ -0,0 +1,29 @@ +package com.nqmgaming.searchaddresslab.core.util + +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin +import kotlin.math.sqrt + +object CalculateDistance { + fun calculateDistance( + lat1: Double, + lon1: Double, + lat2: Double, + lon2: Double + ): Double { + val earthRadius = 6371.0 // radius of the earth in kilometers + + val latDistance = Math.toRadians(lat2 - lat1) + val lonDistance = Math.toRadians(lon2 - lon1) + + val a = sin(latDistance / 2).pow(2.0) + + cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) * + sin(lonDistance / 2).pow(2.0) + + val c = 2 * atan2(sqrt(a), sqrt(1 - a)) + + return earthRadius * c + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nqmgaming/searchaddresslab/data/mapper/ResponseMapper.kt b/app/src/main/java/com/nqmgaming/searchaddresslab/data/mapper/ResponseMapper.kt index 986024d..7be9f57 100644 --- a/app/src/main/java/com/nqmgaming/searchaddresslab/data/mapper/ResponseMapper.kt +++ b/app/src/main/java/com/nqmgaming/searchaddresslab/data/mapper/ResponseMapper.kt @@ -13,6 +13,7 @@ fun Response.toDomainResponse(): DomainResponse { localityType = item.localityType, mapView = item.mapView?.asDomainMapView(), title = item.title, + distance = item.distance ) } ) diff --git a/app/src/main/java/com/nqmgaming/searchaddresslab/data/remote/ApiService.kt b/app/src/main/java/com/nqmgaming/searchaddresslab/data/remote/ApiService.kt index 083b5f5..d51367f 100644 --- a/app/src/main/java/com/nqmgaming/searchaddresslab/data/remote/ApiService.kt +++ b/app/src/main/java/com/nqmgaming/searchaddresslab/data/remote/ApiService.kt @@ -8,11 +8,12 @@ import retrofit2.http.GET import retrofit2.http.Query interface ApiService { - @GET("autocomplete") - suspend fun getGeocode( + @GET("autosuggest") + suspend fun getAddresses( @Query("q") q: String, @Query("apiKey") apiKey: String = API_KEY, -// @Query("at") at: String = CURRENT_LOCATION, this is for auto suggest -// @Query("in") inCountry: String = COUNTRY_CODE + @Query("at") at: String = CURRENT_LOCATION, + @Query("in") inCountry: String = COUNTRY_CODE ): Response + } \ No newline at end of file diff --git a/app/src/main/java/com/nqmgaming/searchaddresslab/data/remote/dto/Item.kt b/app/src/main/java/com/nqmgaming/searchaddresslab/data/remote/dto/Item.kt index 4af724f..9848677 100644 --- a/app/src/main/java/com/nqmgaming/searchaddresslab/data/remote/dto/Item.kt +++ b/app/src/main/java/com/nqmgaming/searchaddresslab/data/remote/dto/Item.kt @@ -8,5 +8,5 @@ data class Item( val politicalView: String?, val position: Position?, val title: String?, - + val distance: Double?, ) \ No newline at end of file diff --git a/app/src/main/java/com/nqmgaming/searchaddresslab/data/repository/HereRepositoryImpl.kt b/app/src/main/java/com/nqmgaming/searchaddresslab/data/repository/HereRepositoryImpl.kt index 6678b19..47f3614 100644 --- a/app/src/main/java/com/nqmgaming/searchaddresslab/data/repository/HereRepositoryImpl.kt +++ b/app/src/main/java/com/nqmgaming/searchaddresslab/data/repository/HereRepositoryImpl.kt @@ -24,7 +24,7 @@ class HereRepositoryImpl @Inject constructor( return flow { emit(Resources.Loading(true)) val hereList = try { - hereApi.getGeocode(q) + hereApi.getAddresses(q) } catch (e: IOException) { Log.e(TAG, "Get geocodes: ${e.stackTraceToString()}") emit(Resources.Error("An error occurred while fetching data")) diff --git a/app/src/main/java/com/nqmgaming/searchaddresslab/domain/model/Item.kt b/app/src/main/java/com/nqmgaming/searchaddresslab/domain/model/Item.kt index 9b3476e..ba948aa 100644 --- a/app/src/main/java/com/nqmgaming/searchaddresslab/domain/model/Item.kt +++ b/app/src/main/java/com/nqmgaming/searchaddresslab/domain/model/Item.kt @@ -7,4 +7,5 @@ data class Item( val mapView: MapView?, val position: Position?, val title: String?, + var distance: Double?, ) \ No newline at end of file diff --git a/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/home/HomeScreen.kt b/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/home/HomeScreen.kt index 5dc4f7f..8764450 100644 --- a/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/home/HomeScreen.kt @@ -1,5 +1,7 @@ package com.nqmgaming.searchaddresslab.presentation.screen.home +import android.Manifest +import android.content.pm.PackageManager import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -32,6 +34,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.app.ActivityCompat import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted @@ -58,7 +61,7 @@ fun HomeScreen( context ) - var title = remember { + val title = remember { mutableStateOf("Permission not granted yet!") } @@ -66,6 +69,16 @@ fun HomeScreen( locationPermission.launchPermissionRequest() if (locationPermission.status.isGranted) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + return@LaunchedEffect + } fusedLocationClient.lastLocation.addOnSuccessListener { location -> // Got last known location. In some rare situations this can be null. if (location != null) { diff --git a/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/SearchScreen.kt b/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/SearchScreen.kt index 016c9d6..5d17bfb 100644 --- a/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/SearchScreen.kt +++ b/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/SearchScreen.kt @@ -1,5 +1,7 @@ package com.nqmgaming.searchaddresslab.presentation.screen.search +import android.Manifest +import android.content.pm.PackageManager import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -23,7 +25,9 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -37,16 +41,24 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import androidx.core.app.ActivityCompat import androidx.hilt.navigation.compose.hiltViewModel import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices import com.nqmgaming.searchaddresslab.R +import com.nqmgaming.searchaddresslab.core.util.CalculateDistance.calculateDistance import com.nqmgaming.searchaddresslab.core.util.NetworkUtils import com.nqmgaming.searchaddresslab.presentation.screen.search.components.AddressItem +@OptIn(ExperimentalPermissionsApi::class) @Composable fun SearchScreen( modifier: Modifier = Modifier, @@ -88,6 +100,51 @@ fun SearchScreen( ) + val locationPermission = rememberPermissionState( + Manifest.permission.ACCESS_FINE_LOCATION + ) + + val fusedLocationClient: FusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient( + context + ) + + val lat = remember { + mutableDoubleStateOf(0.0) + } + val lng = remember { + mutableDoubleStateOf(0.0) + } + + LaunchedEffect(key1 = lat.doubleValue, key2 = lng.doubleValue) { + locationPermission.launchPermissionRequest() + + if (locationPermission.status.isGranted) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + return@LaunchedEffect + } + fusedLocationClient.lastLocation.addOnSuccessListener { location -> + // Got last known location. In some rare situations this can be null. + if (location != null) { + // Logic to handle location object + try { + lat.doubleValue = location.latitude + lng.doubleValue = location.longitude + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + } + if (networkIsConnected) { Column( modifier = modifier.fillMaxSize(), @@ -157,18 +214,30 @@ fun SearchScreen( } ), - ) + ) LazyColumn( modifier = Modifier .fillMaxSize() .padding(top = 8.dp) ) { - addresses.items?.let { - items(it.size) { index -> - val address = it[index] + + addresses.items?.let { items -> + items.forEach { address -> + address.distance = calculateDistance( + lat1 = lat.doubleValue, + lon1 = lng.doubleValue, + lat2 = address.position?.lat ?: 0.0, + lon2 = address.position?.lng ?: 0.0 + ) + } + + val sortedItems = items.sortedBy { address -> address.distance } + + items(sortedItems.size) { index -> + val address = sortedItems[index] AddressItem( item = address, - query = query + query = query, ) } } diff --git a/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/SearchScreenViewModel.kt b/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/SearchScreenViewModel.kt index 5dab373..aa0189c 100644 --- a/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/SearchScreenViewModel.kt +++ b/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/SearchScreenViewModel.kt @@ -66,6 +66,10 @@ class SearchScreenViewModel @Inject constructor( delay(delayMillis) onEvent(SearchScreenEvent.OnSearch(newQuery)) } + }else{ + _state.value = _state.value.copy( + addresses = Response(items = emptyList()) + ) } } diff --git a/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/components/AddressItem.kt b/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/components/AddressItem.kt index 4b89c1e..31ee8b3 100644 --- a/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/components/AddressItem.kt +++ b/app/src/main/java/com/nqmgaming/searchaddresslab/presentation/screen/search/components/AddressItem.kt @@ -2,6 +2,7 @@ package com.nqmgaming.searchaddresslab.presentation.screen.search.components import android.content.Intent import android.net.Uri +import android.util.Log import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -27,20 +28,26 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.nqmgaming.searchaddresslab.R +import com.nqmgaming.searchaddresslab.core.util.CalculateDistance import com.nqmgaming.searchaddresslab.domain.model.Item @Composable fun AddressItem( modifier: Modifier = Modifier, item: Item, - query: String + query: String, ) { val context = LocalContext.current + + Log.d("Distance", "Distance: ${item.distance}") + Column( modifier = modifier.fillMaxWidth() ) { @@ -60,23 +67,38 @@ fun AddressItem( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(32.dp) - .background( - color = Color.Gray.copy(alpha = 0.23f), - shape = CircleShape - ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Icon( - imageVector = Icons.Default.LocationOn, - contentDescription = "LocationOn", + Box( + contentAlignment = Alignment.Center, modifier = Modifier - .size(18.dp) - .padding(2.dp), - tint = Color.Black.copy(alpha = 0.8f) - ) + .size(32.dp) + .background( + color = Color.Gray.copy(alpha = 0.23f), + shape = CircleShape + ) + ) { + Icon( + imageVector = Icons.Default.LocationOn, + contentDescription = "LocationOn", + modifier = Modifier + .size(18.dp) + .padding(2.dp), + tint = Color.Black.copy(alpha = 0.8f) + ) + } + Text(text = item.position?.let { + item.distance?.let { distance -> + if (distance > 1) { + "%.1f km".format(distance) + } else { + "%.1f m".format(distance * 1000) + } + } + } ?: "unknown", style = TextStyle(fontSize = 10.sp)) } Column( modifier = Modifier.weight(1f) @@ -109,16 +131,18 @@ fun AddressItem( modifier = Modifier.padding(horizontal = 10.dp), overflow = TextOverflow.Ellipsis ) + if (label.isNotEmpty()) { + Text( + text = annotateRecursively( + placeHolderList = listOf(Pair(query, labelSpanStyle)), + originalText = label + ), + maxLines = 1, + modifier = Modifier.padding(horizontal = 10.dp), + overflow = TextOverflow.Ellipsis + ) + } - Text( - text = annotateRecursively( - placeHolderList = listOf(Pair(query, labelSpanStyle)), - originalText = label - ), - maxLines = 1, - modifier = Modifier.padding(horizontal = 10.dp), - overflow = TextOverflow.Ellipsis - ) } Icon( painter = painterResource(id = R.drawable.ic_right_square), @@ -127,27 +151,30 @@ fun AddressItem( modifier = Modifier .size(24.dp) .clickable { - if (item.position != null){ - // create google map intent - val position = item.position + if (item.position != null) { + // create google map intent + val position = item.position - /* - * Query the position of the item - * daddr: destination address - * q: query - * mrt: mass rapid transit - * Detail: https://stackoverflow.com/questions/11419407/using-query-string-parameters-with-google-maps-api-v3-services - */ + /* + * Query the position of the item + * daddr: destination address + * q: query + * mrt: mass rapid transit + * Detail: https://stackoverflow.com/questions/11419407/using-query-string-parameters-with-google-maps-api-v3-services + */ - val uri = "http://maps.google.com/maps?daddr=${position.lat},${position.lng}" + val uri = + "http://maps.google.com/maps?daddr=${position.lat},${position.lng}" - // create intent - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) - intent.setPackage("com.google.android.apps.maps") - context.startActivity(intent) - }else{ - Toast.makeText(context, "No position found", Toast.LENGTH_SHORT).show() - } + // create intent + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) + intent.setPackage("com.google.android.apps.maps") + context.startActivity(intent) + } else { + Toast + .makeText(context, "No position found", Toast.LENGTH_SHORT) + .show() + } } .background(