Files
StrassenSchadenPro2/app/src/main/java/de/jadehs/strassenschadenpro2/pages/CreatePage.kt
2025-12-18 09:30:46 +01:00

305 lines
11 KiB
Kotlin

package de.jadehs.strassenschadenpro2.pages
import android.Manifest
import android.content.Context
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.location.LocationDisplayAutoPanMode
import com.arcgismaps.toolkit.geoviewcompose.MapView
import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
import com.google.android.gms.location.LocationServices
import de.jadehs.strassenschadenpro2.AlbumViewModel
import de.jadehs.strassenschadenpro2.MapViewModel
import de.jadehs.strassenschadenpro2.components.DropDownMenuBox
import de.jadehs.strassenschadenpro2.components.album.AlbumViewState
import de.jadehs.strassenschadenpro2.components.album.Intent
import de.jadehs.strassenschadenpro2.components.album.PhotoData
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreatePage(modifier: Modifier = Modifier, mapViewModel: MapViewModel) {
var beschreibungTextFieldValue = remember { mutableStateOf(TextFieldValue("")) }
var currentDamageType = remember { mutableStateOf("")}
var showModalBottomSheet = remember { mutableStateOf(false) }
var enableSaveButton = remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
var albumViewModel = remember { AlbumViewModel(coroutineScope.coroutineContext) }
val locationDisplay = rememberLocationDisplay().apply {
setAutoPanMode(LocationDisplayAutoPanMode.Off)
}
val context = LocalContext.current
if (checkPermissions(context)) {
// Permissions are already granted.
LaunchedEffect(Unit) {
locationDisplay.dataSource.start()
}
} else {
RequestPermissions(
context = context,
onPermissionsGranted = {
coroutineScope.launch {
locationDisplay.dataSource.start()
}
}
)
}
val fuesedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
Column(modifier = modifier.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = beschreibungTextFieldValue.value,
onValueChange = {
beschreibungTextFieldValue.value = it
enableSaveButton.value = beschreibungTextFieldValue.value.text.isNotEmpty() && currentDamageType.value.isNotEmpty()
},
minLines = 5,
label = { Text("Beschreibung")}
)
DropDownMenuBox(
modifier = Modifier
.padding(8.dp),
textFieldLabel = "Select damage type",
textFieldValue = currentDamageType.value,
dropDownItemList = mapViewModel.damageTypeList,
onIndexSelected = { index ->
currentDamageType.value = mapViewModel.damageTypeList[index]
enableSaveButton.value = beschreibungTextFieldValue.value.text.isNotEmpty() && currentDamageType.value.isNotEmpty()
}
)
AlbumScreen(modifier= Modifier, viewModel = albumViewModel)
Row(modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly){
Button(enabled = enableSaveButton.value,
onClick = {
fuesedLocationClient.lastLocation.addOnSuccessListener { location ->
location?.let { location->
val beschreibung = beschreibungTextFieldValue.value.text
val typ = currentDamageType.value
var point = Point(location.longitude,
location.latitude,
SpatialReference.wgs84())
val photoDataList = getPhotosFromAlbum(context,albumViewModel)
mapViewModel.createFeatureWithAttributesAtPoint(
point,
beschreibung,
typ,
photoDataList)
}
}
}){
Text("GPS")
}
Button(enabled = enableSaveButton.value,
onClick = {
showModalBottomSheet.value = !showModalBottomSheet.value
}){
Text("Karte")
}
}
}
if(showModalBottomSheet.value) {
ModalBottomSheet(
sheetGesturesEnabled = false,
onDismissRequest = {
showModalBottomSheet.value = false
}
) {
MapView(
modifier = Modifier,
arcGISMap = mapViewModel.map,
locationDisplay = locationDisplay,
mapViewProxy = mapViewModel.mapViewProxy,
onSingleTapConfirmed = { singleTapConfirmedEvent ->
val screenCoordinate = singleTapConfirmedEvent.screenCoordinate
val beschreibung = beschreibungTextFieldValue.value.text
val typ = currentDamageType.value
val photoDataList=getPhotosFromAlbum(context,albumViewModel)
mapViewModel.createFeatureWithAttributesAt(
screenCoordinate,
beschreibung,
typ,
photoDataList
)
},
)
}
}
}
@Composable
fun AlbumScreen(modifier: Modifier = Modifier, viewModel: AlbumViewModel) {
// collecting the flow from the view model as a state allows our ViewModel and View
// to be in sync with each other.
val viewState: AlbumViewState by viewModel.viewStateFlow.collectAsState()
val currentContext = LocalContext.current
// launches photo picker
val pickImageFromAlbumLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { urls ->
viewModel.onReceive(Intent.OnFinishPickingImagesWith(currentContext, urls))
}
// launches camera
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { isImageSaved ->
if (isImageSaved) {
viewModel.onReceive(Intent.OnImageSavedWith(currentContext))
} else {
// handle image saving error or cancellation
viewModel.onReceive(Intent.OnImageSavingCanceled)
}
}
// launches camera permissions
val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted ->
if (permissionGranted) {
viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
} else {
// handle permission denied such as:
viewModel.onReceive(Intent.OnPermissionDenied)
}
}
// this ensures that the camera is launched only once when the url of the temp file changes
LaunchedEffect(key1 = viewState.tempFileUrl) {
viewState.tempFileUrl?.let {
cameraLauncher.launch(it)
}
}
// basic view that has 2 buttons and a grid for selected pictures
Column(modifier = Modifier.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally) {
Row {
Button(onClick = {
// get user's permission first to use camera
permissionLauncher.launch(Manifest.permission.CAMERA)
}) {
Text(text = "Take a photo")
}
Spacer(modifier = Modifier.width(16.dp))
Button(onClick = {
// Image picker does not require special permissions and can be activated right away
val mediaRequest = PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
pickImageFromAlbumLauncher.launch(mediaRequest)
}) {
Text(text = "Pick a picture")
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(text = "Selected Pictures")
LazyVerticalGrid(modifier = Modifier.fillMaxWidth().heightIn(0.dp, 1200.dp),
columns = GridCells.Adaptive(150.dp),
userScrollEnabled = false) {
itemsIndexed(viewState.selectedPictures) { index, picture ->
Image(modifier = Modifier.padding(8.dp),
bitmap = picture,
contentDescription = null,
contentScale = ContentScale.FillWidth
)
}
}
}
}
fun getPhotosFromAlbum(context: Context,albumViewModel: AlbumViewModel): List<PhotoData>{
var photoDataList = mutableListOf<PhotoData>()
albumViewModel.viewStateFlow.value.imageUris.forEach { uri ->
context.contentResolver.openInputStream(uri).use { inputStream ->
try {
val name = "image_${System.currentTimeMillis()}.png"
val contentType = "image/png"
val byteArray = inputStream?.readBytes()
if (byteArray!=null) {
photoDataList.add(
PhotoData(
name,
contentType,
byteArray
)
)
}
}catch (e: Exception){
}
}
}
return photoDataList
}