Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pick task location #405

Merged
merged 12 commits into from
Jun 2, 2024
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.INTERNET" />

<!-- Always include this permission -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- Include only if your app benefits from precise location access. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />


<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,14 @@ import androidx.lifecycle.LifecycleObserver
import com.github.se.assocify.BuildConfig
import com.github.se.assocify.model.entities.MapMarkerData
import java.io.File
import kotlin.random.Random
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.MapTileProviderBasic
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
import org.osmdroid.tileprovider.tilesource.TileSourceFactory.MAPNIK
import org.osmdroid.util.GeoPoint
import org.osmdroid.util.MapTileIndex
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.TilesOverlay

// Initial position of the map (EPFL Agora)
private val INITIAL_POSITION = GeoPoint(46.518726, 6.566613)
// Initial zoom of the map (Zoom made to be focused on the EPFL campus)
private const val INITIAL_ZOOM = 17.0

/** A screen that displays a map of the event: location with the associated tasks. */
@Composable
fun EventMapScreen(viewModel: EventMapViewModel) {
Expand Down Expand Up @@ -107,8 +98,12 @@ fun rememberLifecycleObserver(mapView: MapView): LifecycleObserver =
remember(mapView) {
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> mapView.onResume()
Lifecycle.Event.ON_PAUSE -> mapView.onPause()
Lifecycle.Event.ON_RESUME -> {
mapView.onResume()
}
Lifecycle.Event.ON_PAUSE -> {
mapView.onPause()
}
else -> {}
}
}
Expand Down Expand Up @@ -153,22 +148,3 @@ fun EPFLMapView(
AndroidView(
factory = { mapViewState }, modifier = modifier, update = { view -> onLoad?.invoke(view) })
}

/**
* The custom tile source from the EPFL plan API.
*
* @param floorId the floor id of the map to display
*/
class CampusTileSource(private val floorId: Int) :
OnlineTileSourceBase("EPFLCampusTileSource", 0, 18, 256, ".png", arrayOf()) {
override fun getTileURLString(pMapTileIndex: Long): String {
// Select at random the map server to use
val epflCampusServerCount = 3
// EPFL plan API has 3 servers, tilesX correspond to the server number
return "https://plan-epfl-tiles${Random.nextInt(epflCampusServerCount)}.epfl.ch/1.0.0/batiments/default/20160712/$floorId/3857/${
MapTileIndex.getZoom(
pMapTileIndex
)
}/${MapTileIndex.getY(pMapTileIndex)}/${MapTileIndex.getX(pMapTileIndex)}.png"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.github.se.assocify.ui.screens.event.maptab

import kotlin.random.Random
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
import org.osmdroid.util.GeoPoint
import org.osmdroid.util.MapTileIndex

// Initial position of the map (EPFL Agora)
val INITIAL_POSITION = GeoPoint(46.518726, 6.566613)
// Initial zoom of the map (Zoom made to be focused on the EPFL campus)
const val INITIAL_ZOOM = 17.0

/**
* The custom tile source from the EPFL plan API.
*
* @param floorId the floor id of the map to display
*/
class CampusTileSource(private val floorId: Int) :
OnlineTileSourceBase("EPFLCampusTileSource", 0, 18, 256, ".png", arrayOf()) {
override fun getTileURLString(pMapTileIndex: Long): String {
// Select at random the map server to use
val epflCampusServerCount = 3
// EPFL plan API has 3 servers, tilesX correspond to the server number
return "https://plan-epfl-tiles${Random.nextInt(epflCampusServerCount)}.epfl.ch/1.0.0/batiments/default/20160712/$floorId/3857/${
MapTileIndex.getZoom(
pMapTileIndex
)
}/${MapTileIndex.getY(pMapTileIndex)}/${MapTileIndex.getX(pMapTileIndex)}.png"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.github.se.assocify.ui.screens.event.tasktab.task

import android.annotation.SuppressLint
import android.graphics.Canvas
import android.view.MotionEvent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand All @@ -9,6 +12,7 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
Expand All @@ -30,28 +34,47 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.PopupProperties
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleObserver
import com.github.se.assocify.BuildConfig
import com.github.se.assocify.navigation.NavigationActions
import com.github.se.assocify.ui.composables.BackButton
import com.github.se.assocify.ui.composables.CenteredCircularIndicator
import com.github.se.assocify.ui.composables.DatePickerWithDialog
import com.github.se.assocify.ui.composables.ErrorMessage
import com.github.se.assocify.ui.composables.TimePickerWithDialog
import com.github.se.assocify.ui.screens.event.maptab.CampusTileSource
import com.github.se.assocify.ui.screens.event.maptab.INITIAL_POSITION
import com.github.se.assocify.ui.screens.event.maptab.INITIAL_ZOOM
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.MapTileProviderBasic
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.TilesOverlay

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TaskScreen(navActions: NavigationActions, viewModel: TaskViewModel) {
val taskState by viewModel.uiState.collectAsState()
val isMapInteracting = remember { mutableStateOf(false) }

Scaffold(
modifier = Modifier.testTag("taskScreen"),
Expand Down Expand Up @@ -85,7 +108,9 @@ fun TaskScreen(navActions: NavigationActions, viewModel: TaskViewModel) {

Column(
modifier =
Modifier.fillMaxSize().padding(paddingValues).verticalScroll(rememberScrollState()),
Modifier.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState(), enabled = !isMapInteracting.value),
verticalArrangement = Arrangement.spacedBy(5.dp),
horizontalAlignment = Alignment.CenterHorizontally) {
Box(
Expand Down Expand Up @@ -172,6 +197,18 @@ fun TaskScreen(navActions: NavigationActions, viewModel: TaskViewModel) {
errorText = { taskState.durationError?.let { Text(it) } },
dialogTitle = "Select Duration",
switchModes = false)

// Problem is here
Polymeth marked this conversation as resolved.
Show resolved Hide resolved
Box(modifier = Modifier.size(300.dp).padding(5.dp)) {
MapPickerView(
modifier = Modifier.fillMaxWidth(),
onLoad = {},
onMapInteraction = { isInteracting ->
isMapInteracting.value = isInteracting // Set the flag based on interaction
},
viewModel = viewModel)
}

Column {
Button(
modifier = Modifier.testTag("saveButton").fillMaxWidth(),
Expand All @@ -198,3 +235,126 @@ fun TaskScreen(navActions: NavigationActions, viewModel: TaskViewModel) {
}
}
}

@SuppressLint("StateFlowValueCalledInComposition")
@Composable
fun rememberMapViewWithLifecycle(): MapView {
val context = LocalContext.current

// Update OSM configuration, for some reason
Configuration.getInstance().userAgentValue = BuildConfig.APPLICATION_ID
Configuration.getInstance().tileFileSystemCacheMaxBytes = 50L * 1024 * 1024

// No need for cache here

// Initialise the map view
val mapView = remember {
MapView(context).apply {
setTileSource(TileSourceFactory.MAPNIK)
// Enable pinch to zoom
setMultiTouchControls(true)
// Initial settings
controller.setZoom(INITIAL_ZOOM)
controller.setCenter(INITIAL_POSITION)
// Sets the tile source ot the EPFL plan tiles
val campusTileSource = CampusTileSource(0)
val tileProvider = MapTileProviderBasic(context, campusTileSource)
val tilesOverlay = TilesOverlay(tileProvider, context)
overlays.add(tilesOverlay)
clipToOutline = true
}
}

// Make the mapview live as long as the composable
val lifecycleObserver = rememberMapLifecycleObserver(mapView)
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle) {
lifecycle.addObserver(lifecycleObserver)
onDispose { lifecycle.removeObserver(lifecycleObserver) }
}

return mapView
}

@Composable
fun rememberMapLifecycleObserver(mapView: MapView): LifecycleObserver =
remember(mapView) {
LifecycleEventObserver { _, event ->
when (event) {
// Lifecycle.Event.ON_CREATE -> mapView.onCreate(null)
Polymeth marked this conversation as resolved.
Show resolved Hide resolved
// Lifecycle.Event.ON_START -> mapView.onStart()
Lifecycle.Event.ON_RESUME -> mapView.onResume()
Lifecycle.Event.ON_PAUSE -> mapView.onPause()
// Lifecycle.Event.ON_STOP -> mapView.onStop()
// Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
else -> {}
}
}
}

@SuppressLint("ClickableViewAccessibility")
@Composable
fun MapPickerView(
modifier: Modifier = Modifier,
onLoad: ((map: MapView) -> Unit)? = null,
onMapInteraction: (Boolean) -> Unit, // Callback to notify interaction
viewModel: TaskViewModel
) {
val mapView = rememberMapViewWithLifecycle()
val currentMarker = remember { mutableStateOf<Marker?>(null) }

AndroidView(
factory = { mapView },
modifier = modifier,
update = { view ->
onLoad?.invoke(view)
// Handle touch events for interaction detection
view.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_MOVE -> {
onMapInteraction(true) // Notify interaction when touch starts or moves
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
onMapInteraction(false) // Notify end of interaction when touch ends or is cancelled
}
}
false // Return false to allow touch event to propagate
}

// Handle map clicks to add marker
view.overlays.add(
object : org.osmdroid.views.overlay.Overlay() {

override fun draw(c: Canvas?, osmv: MapView?, shadow: Boolean) {
// Overriding draw is required but we don't need to do anything here
}

override fun onSingleTapConfirmed(e: MotionEvent?, mapView: MapView?): Boolean {
e?.let {
val geoPoint = mapView?.projection?.fromPixels(e.x.toInt(), e.y.toInt())
geoPoint?.let { point ->
// Remove the old marker if exists
currentMarker.value.let { mapView.overlays.remove(it) }

// Add new marker
val marker =
Marker(mapView).apply {
position = point as GeoPoint?
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
}

mapView.overlays.add(marker)
currentMarker.value = marker

viewModel.setLocation(marker.position.toDoubleString())

mapView.invalidate() // Refresh the map to show the new marker
}
}
return true
}
})
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ class TaskViewModel {
}
}

fun setLocation(location: String) {
_uiState.value = _uiState.value.copy(location = location)
}

fun setDescription(description: String) {
_uiState.value = _uiState.value.copy(description = description)
}
Expand Down Expand Up @@ -211,7 +215,7 @@ class TaskViewModel {
duration = duration,
peopleNeeded = _uiState.value.staffNumber.toInt(),
category = _uiState.value.category,
location = "", // TODO: Add location
location = _uiState.value.location, // TODO: Add location
Polymeth marked this conversation as resolved.
Show resolved Hide resolved
eventUid = event.uid,
)

Expand Down Expand Up @@ -268,6 +272,7 @@ data class TaskState(
val time: String = "",
val duration: String = "",
val event: Event? = null,
val location: String = "",
Polymeth marked this conversation as resolved.
Show resolved Hide resolved
val eventList: List<Event> = emptyList(),
val titleError: String? = null,
val staffNumberError: String? = null,
Expand Down
Loading