Skip to content

Commit

Permalink
Merge pull request #187 from Swent-team-6/feature/map
Browse files Browse the repository at this point in the history
Feature/map : HeatMap improvements & Meeting requests
  • Loading branch information
JanStaszewiczEPFL authored Dec 5, 2024
2 parents 1dcd14e + 08f5268 commit 603ae3d
Show file tree
Hide file tree
Showing 13 changed files with 362 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,6 @@ class LocationMapSelectorTest {
"Let's meet on the second floor",
Pair(DEFAULT_USER_LATITUDE, DEFAULT_USER_LONGITUDE))))
verify(mockFunctions).getHttpsCallable("sendMeetingConfirmation")
verify(mockNavigationActions).navigateTo(Route.HEAT_MAP)
verify(mockNavigationActions).navigateTo(Route.MAP)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import com.github.se.icebreakrr.model.profile.ProfilePicRepositoryStorage
import com.github.se.icebreakrr.model.profile.ProfilesRepository
import com.github.se.icebreakrr.model.profile.ProfilesViewModel
import com.github.se.icebreakrr.ui.navigation.NavigationActions
import com.github.se.icebreakrr.ui.profile.HeatMap
import com.github.se.icebreakrr.ui.profile.MapScreen
import com.github.se.icebreakrr.utils.IPermissionManager
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.storage.FirebaseStorage
Expand All @@ -21,7 +21,7 @@ import org.junit.runner.RunWith
import org.mockito.Mockito.*

@RunWith(AndroidJUnit4::class)
class HeatMapTest {
class MapTest {

@get:Rule val composeTestRule = createComposeRule()

Expand Down Expand Up @@ -54,16 +54,20 @@ class HeatMapTest {
}

@Test
fun testHeatMapScreenDisplaysCorrectly() {
composeTestRule.setContent { HeatMap(navigationActions, profilesViewModel, locationViewModel) }
fun testMapScreenDisplaysCorrectly() {
composeTestRule.setContent {
MapScreen(navigationActions, profilesViewModel, locationViewModel)
}

// Verify basic UI elements are displayed
composeTestRule.onNodeWithTag("heatMapScreen").assertIsDisplayed()
composeTestRule.onNodeWithTag("MapScreen").assertIsDisplayed()
}

@Test
fun testBottomNavigationDisplaysCorrectly() {
composeTestRule.setContent { HeatMap(navigationActions, profilesViewModel, locationViewModel) }
composeTestRule.setContent {
MapScreen(navigationActions, profilesViewModel, locationViewModel)
}

// Verify bottom navigation is displayed
composeTestRule.onNodeWithTag("bottomNavigationMenu").assertIsDisplayed()
Expand Down
10 changes: 4 additions & 6 deletions app/src/main/java/com/github/se/icebreakrr/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ import com.github.se.icebreakrr.ui.map.LocationSelectorMapScreen
import com.github.se.icebreakrr.ui.navigation.NavigationActions
import com.github.se.icebreakrr.ui.navigation.Route
import com.github.se.icebreakrr.ui.navigation.Screen
import com.github.se.icebreakrr.ui.profile.HeatMap
import com.github.se.icebreakrr.ui.profile.InboxProfileViewScreen
import com.github.se.icebreakrr.ui.profile.MapScreen
import com.github.se.icebreakrr.ui.profile.OtherProfileView
import com.github.se.icebreakrr.ui.profile.ProfileEditingScreen
import com.github.se.icebreakrr.ui.profile.ProfileView
Expand Down Expand Up @@ -406,12 +406,10 @@ fun IcebreakrrNavHost(
}

navigation(
startDestination = Screen.HEAT_MAP,
route = Route.HEAT_MAP,
startDestination = Screen.MAP,
route = Route.MAP,
) {
composable(Screen.HEAT_MAP) {
HeatMap(navigationActions, profileViewModel, locationViewModel)
}
composable(Screen.MAP) { MapScreen(navigationActions, profileViewModel, locationViewModel) }
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/com/github/se/icebreakrr/model/map/Map.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.github.se.icebreakrr.model.map

import androidx.compose.ui.geometry.Offset
import com.google.android.gms.maps.model.LatLng

data class UserMarker(
val uid: String,
val username: String,
val location: LatLng, // Assuming you have the location as LatLng
var overlayPosition: Offset? = null // Optional property for overlay position
)
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ fun LocationSelectorMapScreen(
Toast.makeText(context, "Could not send meeting location", Toast.LENGTH_SHORT)
.show()
}
navigationActions.navigateTo(Route.HEAT_MAP)
navigationActions.navigateTo(Route.MAP)
}
},
modifier =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,58 @@ import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.github.se.icebreakrr.model.location.LocationViewModel
import com.github.se.icebreakrr.model.map.UserMarker
import com.github.se.icebreakrr.model.profile.Profile
import com.github.se.icebreakrr.model.profile.ProfilesViewModel
import com.github.se.icebreakrr.ui.navigation.BottomNavigationMenu
import com.github.se.icebreakrr.ui.navigation.LIST_TOP_LEVEL_DESTINATIONS
import com.github.se.icebreakrr.ui.navigation.NavigationActions
import com.github.se.icebreakrr.ui.navigation.Route
import com.github.se.icebreakrr.ui.navigation.Screen
import com.github.se.icebreakrr.utils.cropUsername
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.firebase.firestore.GeoPoint
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.TileOverlay
import com.google.maps.android.compose.rememberCameraPositionState
import com.google.maps.android.compose.rememberMarkerState
import com.google.maps.android.heatmaps.Gradient
import com.google.maps.android.heatmaps.HeatmapTileProvider
import com.google.maps.android.heatmaps.WeightedLatLng
import kotlin.math.roundToInt

// Constants for default values
private val DEFAULT_LOCATION = LatLng(46.5197, 6.6323) // EPFL coordinates
private const val DEFAULT_ZOOM = 15f
private const val DEFAULT_RADIUS = 10000 // Radius in meters
Expand All @@ -43,6 +65,13 @@ private const val HEATMAP_RADIUS = 50 // Radius for heatmap points
private const val HEATMAP_OPACITY = 0.8 // Opacity for heatmap
private const val HEATMAP_MAX_INTENSITY = 15.0 // Maximum intensity for heatmap

// Padding constants
private val BUTTON_PADDING = 16.dp
private val LOADING_BOX_PADDING = 16.dp
private val OVERLAY_WIDTH = 90.dp // Width of the overlay
private val OVERLAY_HEIGHT = 25.dp // Height of the overlay
private val OVERLAY_OFFSET_Y = 10 // Offset for overlay position

// Define custom gradient colors (from blue to red)
private val GRADIENT_COLORS =
intArrayOf(
Expand All @@ -67,31 +96,72 @@ private val GRADIENT_START_POINTS =
val gradient = Gradient(GRADIENT_COLORS, GRADIENT_START_POINTS)

@Composable
fun HeatMap(
fun MapScreen(
navigationActions: NavigationActions,
profilesViewModel: ProfilesViewModel,
locationViewModel: LocationViewModel,
) {

val userLocation = locationViewModel.lastKnownLocation.collectAsState()
val profiles = profilesViewModel.filteredProfiles.collectAsState()
val myProfile = profilesViewModel.selfProfile.collectAsState()

var heatmapProvider by remember { mutableStateOf<HeatmapTileProvider?>(null) }
var isMapLoaded by remember { mutableStateOf(false) }
var lastCameraPosition by remember { mutableStateOf<LatLng?>(null) }
var isHeatmapVisible by remember { mutableStateOf(true) }

// Observe the meetingRequestChosenLocalisation
val meetingRequestChosenLocalisation =
myProfile.value?.meetingRequestChosenLocalisation ?: emptyMap()

// Create a list of UIDs to fetch profiles
val uidsToFetch = meetingRequestChosenLocalisation.keys.toList()

// Create a mutable list to hold the fetched profiles
val profilesMeeting = remember { mutableStateListOf<Profile>() }

// Fetch profiles for the UIDs
LaunchedEffect(uidsToFetch) {
if (uidsToFetch.isNotEmpty()) {
profilesMeeting.clear()
uidsToFetch.forEach { uid ->
profilesViewModel.getProfileByUidAndThen(uid) {
// Add the profile to the list after fetching
profilesViewModel.selectedProfile.value?.let { profile -> profilesMeeting.add(profile) }
}
}
}
}

// Create UserMarkers from meetingRequestChosenLocalisation
val userMarkers =
meetingRequestChosenLocalisation.mapNotNull { (uid, pair) ->
val (message, coordinates) = pair
val profile = profilesMeeting.find { it.uid == uid }
profile?.let {
UserMarker(
uid = uid,
username = cropUsername(it.name, 10), // Crop username to 10 characters
location = LatLng(coordinates.first, coordinates.second),
overlayPosition = null // Initially set to null
)
}
}

val markerStates =
userMarkers.map { userMarker -> rememberMarkerState(position = userMarker.location) }

Scaffold(
modifier = Modifier.testTag("heatMapScreen"),
modifier = Modifier.testTag("MapScreen"),
bottomBar = {
BottomNavigationMenu(
onTabSelect = { route ->
if (route.route != Route.HEAT_MAP) {
if (route.route != Route.MAP) {
navigationActions.navigateTo(route)
}
},
tabList = LIST_TOP_LEVEL_DESTINATIONS,
selectedItem = Route.HEAT_MAP,
selectedItem = Route.MAP,
notificationCount = myProfile.value?.meetingRequestInbox?.size ?: 0,
heatMapCount = myProfile.value?.meetingRequestChosenLocalisation?.size ?: 0)
}) { paddingValues ->
Expand Down Expand Up @@ -134,9 +204,17 @@ fun HeatMap(
.build()
} else {
// Optionally handle the case where there are no valid locations
Log.w("HeatMap", "No valid locations to display on the heatmap.")
Log.w("MapScreen", "No valid locations to display on the heatmap.")
}
}

// Example of cleaning up profilesMeeting when no longer needed
DisposableEffect(Unit) {
onDispose {
profilesMeeting.clear() // Clear the list when the composable is disposed
}
}

GoogleMap(
modifier = Modifier.fillMaxSize().padding(paddingValues).testTag("googleMap"),
cameraPositionState = cameraPositionState,
Expand All @@ -147,12 +225,56 @@ fun HeatMap(
center = GeoPoint(center.latitude, center.longitude),
radiusInMeters = DEFAULT_RADIUS)
lastCameraPosition = center
},
onMapClick = {
// Update overlay position when the map is clicked (optional)
}) {
heatmapProvider?.let { provider ->
TileOverlay(tileProvider = provider, transparency = 0.0f)
if (isHeatmapVisible) {
heatmapProvider?.let { provider ->
TileOverlay(tileProvider = provider, transparency = 0.0f)
}
}

userMarkers.forEachIndexed { index, userMarker ->
val markerState = markerStates[index]

// Add the marker to the map
Marker(
contentDescription = "Marker for ${userMarker.username}",
state = markerState,
title = userMarker.username,
snippet = "This is ${userMarker.username}'s location",
onClick = {
// Handle marker click, e.g., navigate to user profile
navigationActions.navigateTo(
Screen.OTHER_PROFILE_VIEW + "?userId=${userMarker.uid}")
true // Return true to indicate the event was handled
})

// Update overlay position based on the marker's position
val projection = cameraPositionState.projection
val markerScreenPosition = projection?.toScreenLocation(userMarker.location)
if (markerScreenPosition != null) {
userMarker.overlayPosition =
Offset(markerScreenPosition.x.toFloat(), markerScreenPosition.y.toFloat())
}
}
}

userMarkers.forEach { userMarker ->
// Use the MarkerOverlay composable to display the text above the marker
userMarker.overlayPosition?.let {
MarkerOverlay(position = it, text = userMarker.username)
}
}

// Toggle button for heatmap visibility
Box(modifier = Modifier.padding(BUTTON_PADDING)) {
Button(onClick = { isHeatmapVisible = !isHeatmapVisible }) {
Text(if (isHeatmapVisible) "Hide Heatmap" else "Show Heatmap")
}
}

// Fetch profiles when the camera position changes
LaunchedEffect(cameraPositionState.position) {
if (isMapLoaded) {
Expand Down Expand Up @@ -199,3 +321,31 @@ fun HeatMap(
}
}
}

@Composable
fun MarkerOverlay(position: Offset, text: String) {
Box(
modifier =
Modifier.offset {
IntOffset(
position.x.roundToInt() - (OVERLAY_WIDTH.toPx() / 2).roundToInt(),
position.y.roundToInt() + OVERLAY_OFFSET_Y) // Center under the pin
}
.size(OVERLAY_WIDTH, OVERLAY_HEIGHT) // Set the size of the overlay
.testTag("markerOverlay")
.background(
Color.Black.copy(alpha = 0.5f),
shape = RoundedCornerShape(20.dp) // Rounded corners
)) {
Text(
text = text,
color = Color.White, // White text for contrast
fontSize = 9.sp, // Change this value to adjust the font size
fontWeight = FontWeight.Bold,
modifier =
Modifier.align(Alignment.Center) // Center the text within the box
.wrapContentSize() // Ensure the text wraps correctly
.padding(2.dp) // Optional: Add padding to the text for better spacing
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ fun BottomNavigationMenu(
if (tab.route == Route.NOTIFICATIONS && notificationCount > 0) {
Badge(notificationCount, "badgeNotification")
}
if (tab.route == Route.HEAT_MAP && heatMapCount > 0) {
if (tab.route == Route.MAP && heatMapCount > 0) {
Badge(heatMapCount, "badgeHeatmap")
}
}
Expand Down
Loading

0 comments on commit 603ae3d

Please sign in to comment.