Skip to content

Commit

Permalink
Merge pull request #256 from LookUpGroup27/feature/clickable-post-loc…
Browse files Browse the repository at this point in the history
…ations

Add clickable location with map navigation
  • Loading branch information
rihabbelmekki authored Dec 19, 2024
2 parents 2a66d7c + c76e32c commit 6917a3f
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class GoogleMapScreenTest {
@Test
fun testEnableLocationButtonIsDisplayed() {
composeTestRule.setContent {
GoogleMapScreen(navigationActions, postsViewModel, profileViewModel, true)
GoogleMapScreen(navigationActions, postsViewModel, profileViewModel, testNoLoca = true)
}

// Verify the UI elements are displayed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.github.lookupgroup27.lookup.ui.calendar.CalendarViewModel
import com.github.lookupgroup27.lookup.ui.feed.FeedScreen
import com.github.lookupgroup27.lookup.ui.fullscreen.FullScreenImageScreen
import com.github.lookupgroup27.lookup.ui.googlemap.GoogleMapScreen
import com.github.lookupgroup27.lookup.ui.googlemap.components.SelectedPostMarker
import com.github.lookupgroup27.lookup.ui.image.CameraCapture
import com.github.lookupgroup27.lookup.ui.image.EditImageScreen
import com.github.lookupgroup27.lookup.ui.image.EditImageViewModel
Expand Down Expand Up @@ -119,8 +120,49 @@ fun LookUpApp() {
composable(Screen.MENU) { MenuScreen(navigationActions, avatarViewModel) }
composable(Screen.PROFILE) { ProfileScreen(navigationActions, avatarViewModel) }
composable(Screen.CALENDAR) { CalendarScreen(calendarViewModel, navigationActions) }
composable(
route = "${Route.GOOGLE_MAP}/{postId}/{lat}/{lon}/{autoCenter}",
arguments =
listOf(
navArgument("postId") {
type = NavType.StringType
nullable = true
},
navArgument("lat") {
type = NavType.FloatType
defaultValue = 0f
},
navArgument("lon") {
type = NavType.FloatType
defaultValue = 0f
},
navArgument("autoCenter") {
type = NavType.BoolType
defaultValue = true
})) { backStackEntry ->
val postId = backStackEntry.arguments?.getString("postId")
val lat = backStackEntry.arguments?.getFloat("lat")?.toDouble() ?: 0.0
val lon = backStackEntry.arguments?.getFloat("lon")?.toDouble() ?: 0.0
val autoCenter = backStackEntry.arguments?.getBoolean("autoCenter") ?: true

val selectedMarker =
if (postId != null) {
SelectedPostMarker(postId, lat, lon)
} else null

GoogleMapScreen(
navigationActions = navigationActions,
postsViewModel = postsViewModel,
profileViewModel = profileViewModel,
selectedPostMarker = selectedMarker,
initialAutoCenterEnabled = autoCenter)
}

composable(Screen.GOOGLE_MAP) {
GoogleMapScreen(navigationActions, postsViewModel, profileViewModel)
GoogleMapScreen(
navigationActions = navigationActions,
postsViewModel = postsViewModel,
profileViewModel = profileViewModel)
}
composable(Screen.QUIZ) { QuizScreen(quizViewModel, navigationActions) }
composable(Screen.PLANET_SELECTION) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ class CollectionRepositoryFirestore(
* @return A list of image URLs associated with the user's email.
*/
override fun getUserPosts(onSuccess: (List<Post>?) -> Unit, onFailure: (Exception) -> Unit) {

val userMail = auth.currentUser?.email
if (userMail == null) {
onFailure(Exception("User is not authenticated."))
Expand All @@ -55,32 +54,38 @@ class CollectionRepositoryFirestore(
onFailure(exception)
return@addSnapshotListener
}

if (snapshot != null) {
val posts =
snapshot.documents
.map { post ->
Log.d(tag, "${post.id} => ${post.data}")
val data = post.data ?: return@map null
if (data["userMail"] as String == userMail) {
Post(
data["uid"] as String,
data["uri"] as String,
data["username"] as String,
data["userMail"] as String,
(data["starsCount"] as? Long)?.toInt() ?: 0,
(data["averageStars"] as? Double) ?: 0.0,
data["latitude"] as Double,
data["longitude"] as Double,
(data["usersNumber"] as? Long)?.toInt() ?: 0,
data["ratedBy"] as? List<String> ?: emptyList(),
data["description"] as? String ?: "",
(data["timestamp"] as? Long) ?: 0L)
} else {
null
}
}
.filterNotNull()
onSuccess(posts)
try {
val posts =
snapshot.documents.mapNotNull { post ->
Log.d(tag, "${post.id} => ${post.data}")
val data = post.data ?: return@mapNotNull null

// Safely cast userMail and compare
val postUserMail = data["userMail"] as? String ?: return@mapNotNull null
if (postUserMail != userMail) return@mapNotNull null

// Safely retrieve all fields with null checks and default values
Post(
uid = data["uid"] as? String ?: return@mapNotNull null,
uri = data["uri"] as? String ?: return@mapNotNull null,
username = data["username"] as? String ?: return@mapNotNull null,
userMail = postUserMail,
starsCount = (data["starsCount"] as? Long)?.toInt() ?: 0,
averageStars = (data["averageStars"] as? Double) ?: 0.0,
latitude = (data["latitude"] as? Double) ?: 0.0,
longitude = (data["longitude"] as? Double) ?: 0.0,
usersNumber = (data["usersNumber"] as? Long)?.toInt() ?: 0,
ratedBy = (data["ratedBy"] as? List<String>) ?: emptyList(),
description = data["description"] as? String ?: "",
timestamp = (data["timestamp"] as? Long) ?: 0L)
}
onSuccess(posts)
} catch (e: Exception) {
Log.e(tag, "Error parsing posts", e)
onFailure(e)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,38 @@ class PostsRepositoryFirestore(private val db: FirebaseFirestore) : PostsReposit
onFailure(exception)
return@addSnapshotListener
}

if (snapshot != null) {
val posts =
snapshot.documents
.map { post ->
Log.d(tag, "${post.id} => ${post.data}")
val data = post.data ?: return@map null
try {
val posts =
snapshot.documents.mapNotNull { post ->
Log.d(tag, "${post.id} => ${post.data}")
val data = post.data ?: return@mapNotNull null

try {
Post(
data["uid"] as String,
data["uri"] as String,
data["username"] as String,
data["userMail"] as String,
(data["starsCount"] as? Long)?.toInt() ?: 0,
(data["averageStars"] as? Double) ?: 0.0,
data["latitude"] as Double,
data["longitude"] as Double,
(data["usersNumber"] as? Long)?.toInt() ?: 0,
data["ratedBy"] as? List<String> ?: emptyList(),
data["description"] as? String ?: "",
(data["timestamp"] as? Long) ?: 0L)
uid = data["uid"] as? String ?: return@mapNotNull null,
uri = data["uri"] as? String ?: return@mapNotNull null,
username = data["username"] as? String ?: return@mapNotNull null,
userMail = data["userMail"] as? String ?: return@mapNotNull null,
starsCount = (data["starsCount"] as? Long)?.toInt() ?: 0,
averageStars = (data["averageStars"] as? Double) ?: 0.0,
latitude = (data["latitude"] as? Double) ?: 0.0,
longitude = (data["longitude"] as? Double) ?: 0.0,
usersNumber = (data["usersNumber"] as? Long)?.toInt() ?: 0,
ratedBy = (data["ratedBy"] as? List<String>) ?: emptyList(),
description = data["description"] as? String ?: "",
timestamp = (data["timestamp"] as? Long) ?: 0L)
} catch (e: Exception) {
Log.e(tag, "Error parsing post document: ${post.id}", e)
null
}
.filterNotNull()
onSuccess(posts)
}
onSuccess(posts)
} catch (e: Exception) {
Log.e(tag, "Error processing posts", e)
onFailure(e)
}
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/com/github/lookupgroup27/lookup/ui/feed/Feed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.github.lookupgroup27.lookup.model.location.LocationProviderSingleton
import com.github.lookupgroup27.lookup.model.post.Post
import com.github.lookupgroup27.lookup.model.profile.UserProfile
import com.github.lookupgroup27.lookup.ui.feed.components.PostItem
import com.github.lookupgroup27.lookup.ui.googlemap.components.SelectedPostMarker
import com.github.lookupgroup27.lookup.ui.navigation.BottomNavigationMenu
import com.github.lookupgroup27.lookup.ui.navigation.LIST_TOP_LEVEL_DESTINATION
import com.github.lookupgroup27.lookup.ui.navigation.NavigationActions
Expand Down Expand Up @@ -270,6 +271,15 @@ fun FeedScreen(
oldStarCounts = oldStarCounts)
postsViewModel.updatePost(updatedPost)
},
onAddressClick = { clickedPost ->
val selectedMarker =
SelectedPostMarker(
postId = clickedPost.uid,
latitude = clickedPost.latitude,
longitude = clickedPost.longitude)
navigationActions.navigateToMapWithPost(
post.uid, post.latitude, post.longitude, false)
},
onImageClick = { imageUrl, username, description ->
navigationActions.navigateToFullScreen(
imageUrl, username, description)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,23 @@ import org.json.JSONObject
* @param onImageClick Callback invoked when the user clicks on the post image. This function should
* navigate to the full-screen image screen. It provides [imageUrl], [username], and [description]
* parameters.
* @param onAddressClick The callback to be invoked when the address is clicked
* @param color The text color for textual content.
* @param textForUsername The display text for the username (or user-related info).
* @param showAverage Flag indicating if the average rating should be displayed.
* @param showAddress Whether to display the address
*/
@Composable
fun PostItem(
post: Post,
starStates: List<Boolean>,
onRatingChanged: (List<Boolean>) -> Unit,
onAddressClick: (Post) -> Unit = {},
onImageClick: (imageUrl: String, username: String, description: String) -> Unit,
color: Color = Color.White,
textForUsername: String = post.username,
showAverage: Boolean = true
showAverage: Boolean = true,
showAddress: Boolean = true
) {
val address = remember { mutableStateOf("Loading address...") }
LaunchedEffect(post.latitude, post.longitude) {
Expand Down Expand Up @@ -75,7 +79,8 @@ fun PostItem(
style = MaterialTheme.typography.bodySmall.copy(color = Color.LightGray),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.testTag("AddressTag_${post.uid}"))
modifier =
Modifier.testTag("AddressTag_${post.uid}").clickable { onAddressClick(post) })

// Image (Clickable)
Image(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ private const val NUMBER_OF_STARS: Int = 3
* @param navigationActions Actions to navigate to different screens.
* @param postsViewModel ViewModel to fetch posts.
* @param profileViewModel ViewModel to fetch user profile.
* @param selectedPostMarker The selected post marker.
* @param initialAutoCenterEnabled Whether auto-centering is enabled.
* @param testNoLoc use for test purpose: it simulates the case before user grants location
* permission
*/
Expand All @@ -50,6 +52,8 @@ fun GoogleMapScreen(
navigationActions: NavigationActions,
postsViewModel: PostsViewModel = viewModel(),
profileViewModel: ProfileViewModel = viewModel(),
selectedPostMarker: SelectedPostMarker? = null,
initialAutoCenterEnabled: Boolean = true,
testNoLoca: Boolean = false
) {
val context = LocalContext.current
Expand All @@ -59,7 +63,9 @@ fun GoogleMapScreen(
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED)
}
var autoCenteringEnabled by remember { mutableStateOf(true) }
var autoCenteringEnabled by remember {
mutableStateOf(initialAutoCenterEnabled)
} // New state for auto-centering
val auth = remember { FirebaseAuth.getInstance() }
val isLoggedIn = auth.currentUser != null
val allPosts by postsViewModel.allPosts.collectAsState()
Expand All @@ -72,6 +78,7 @@ fun GoogleMapScreen(
val bio by remember { mutableStateOf(profile?.bio ?: "") }
val email by remember { mutableStateOf(userEmail) }
val postRatings = remember { mutableStateMapOf<String, List<Boolean>>() }
var highlightedPost by remember(selectedPostMarker) { mutableStateOf(selectedPostMarker) }

// Permission request launcher
val permissionLauncher =
Expand Down Expand Up @@ -112,6 +119,8 @@ fun GoogleMapScreen(
}
}

LaunchedEffect(selectedPostMarker) { highlightedPost = selectedPostMarker }

Scaffold(
modifier = Modifier.testTag("googleMapScreen"),
bottomBar = {
Expand Down Expand Up @@ -193,7 +202,8 @@ fun GoogleMapScreen(
usersNumber = newUsersNumber,
ratedBy = newRatedBy))
},
postRatings = postRatings)
postRatings = postRatings,
highlightedPost = highlightedPost)
}
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ import com.google.maps.android.compose.MarkerState
*
* @param post The post to add a marker for
* @param onMarkerClick The callback to be called when the marker is clicked
* @param isHighlighted Whether the marker should be highlighted
*/
@Composable
fun AddMapMarker(post: Post, onMarkerClick: (Post) -> Unit) {
fun AddMapMarker(post: Post, onMarkerClick: (Post) -> Unit, isHighlighted: Boolean = false) {
val latLng = LatLng(post.latitude, post.longitude)
Marker(
state = MarkerState(position = latLng),
title = post.username,
icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE),
icon =
BitmapDescriptorFactory.defaultMarker(
if (isHighlighted) BitmapDescriptorFactory.HUE_RED
else BitmapDescriptorFactory.HUE_AZURE),
onClick = {
onMarkerClick(post)
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.google.maps.android.compose.*
* @param profile The user's profile
* @param updatePost Function to update a post
* @param postRatings The ratings for each post
* @param highlightedPost The highlighted post
*/
@Composable
fun MapView(
Expand All @@ -37,7 +38,8 @@ fun MapView(
updateProfile: (UserProfile?, MutableMap<String, Int>?) -> Unit,
profile: UserProfile?,
updatePost: (Post, Double, Int, Int, List<String>) -> Unit,
postRatings: MutableMap<String, List<Boolean>>
postRatings: MutableMap<String, List<Boolean>>,
highlightedPost: SelectedPostMarker?
) {

var mapProperties by remember {
Expand Down Expand Up @@ -71,9 +73,17 @@ fun MapView(
LaunchedEffect(profile, posts) {
posts.forEach { post ->
val starsCount = postRatings[post.uid]?.count { it } ?: 0
val usersNumber = postRatings[post.uid]?.size ?: 0
val avg = if (usersNumber == 0) 0.0 else starsCount.toDouble() / usersNumber
updatePost(post, avg, starsCount, usersNumber, post.ratedBy)
val avg = if (post.usersNumber == 0) 0.0 else starsCount.toDouble() / post.usersNumber
updatePost(post, avg, starsCount, post.usersNumber, post.ratedBy)
}
}

LaunchedEffect(highlightedPost) {
highlightedPost?.let { post ->
// Zoom to highlighted marker position with zoom level 15f
val latLng = LatLng(post.latitude, post.longitude)
val cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 15f)
cameraPositionState.animate(cameraUpdate)
}
}

Expand All @@ -84,10 +94,14 @@ fun MapView(
cameraPositionState = cameraPositionState) {
// Add markers for each post
posts.forEach { post ->
val isHighlighted = highlightedPost?.postId == post.uid
Log.d(
"MapView",
"Adding marker at (${post.latitude}, ${post.longitude}) for URI: ${post.uri}")
AddMapMarker(post) { clickedPost -> selectedPost = clickedPost }
AddMapMarker(
post,
onMarkerClick = { clickedPost -> selectedPost = clickedPost },
isHighlighted = isHighlighted)
}

// Display the ImagePreviewDialog when a post is selected
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.github.lookupgroup27.lookup.ui.googlemap.components

data class SelectedPostMarker(val postId: String, val latitude: Double, val longitude: Double)
Loading

0 comments on commit 6917a3f

Please sign in to comment.