- Featurelayer ArcGIS hinzugefügt

- Textbox jetzt nutzbar
- Schadenauswahl Dropdown Menü
- Featureerstellung nimmt eigene Position und wird hinzugefügt.
This commit is contained in:
2026-01-07 18:03:50 +01:00
parent 91b885f67c
commit f5ac96807c
4 changed files with 245 additions and 164 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-12-19T16:43:39.005516700Z"> <DropdownSelection timestamp="2025-12-21T13:16:32.335138100Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=N0AA003656K80600629" /> <DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\sklau\.android\avd\Medium_Phone.avd" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@@ -58,6 +58,7 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.material3)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
@@ -74,4 +75,8 @@ dependencies {
implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom))
implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose)
implementation(libs.arcgis.maps.kotlin.toolkit.authentication) implementation(libs.arcgis.maps.kotlin.toolkit.authentication)
} }

View File

@@ -1,5 +1,6 @@
package com.example.snapandsolve package com.example.snapandsolve
import MapViewModel
import android.Manifest import android.Manifest
import android.app.Application import android.app.Application
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
@@ -7,17 +8,7 @@ import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.lazy.grid.itemsIndexed
@@ -26,110 +17,62 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Filter
import androidx.compose.material.icons.filled.FilterAlt import androidx.compose.material.icons.filled.FilterAlt
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Person import androidx.compose.material3.*
import androidx.compose.material3.BottomAppBar import androidx.compose.runtime.*
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeFloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
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.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
// Hier holen wir die ArcGIS Klassen
import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.toolkit.geoviewcompose.MapView import com.arcgismaps.toolkit.geoviewcompose.MapView
// Hier deine eigenen Klassen (Pfade prüfen!)
import com.example.snapandsolve.camera.AlbumViewModel import com.example.snapandsolve.camera.AlbumViewModel
import com.example.snapandsolve.camera.AlbumViewState import com.example.snapandsolve.camera.AlbumViewState
import com.example.snapandsolve.camera.Intent import com.example.snapandsolve.camera.Intent
import com.example.snapandsolve.ui.theme.AppColor import com.example.snapandsolve.ui.theme.*
import com.example.snapandsolve.ui.theme.ButtonColor
import com.example.snapandsolve.ui.theme.SideSlider
import com.example.snapandsolve.ui.theme.SliderMenuItem
import com.example.snapandsolve.ui.theme.WidgetColor
import com.example.snapandsolve.ui.theme.setupLocationDisplay
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@Composable @Composable
fun MainScreen(modifier: Modifier = Modifier, application: Application) { fun MainScreen(modifier: Modifier = Modifier, application: Application) {
var showReport by rememberSaveable { mutableStateOf(false) } var showReport by rememberSaveable { mutableStateOf(false) }
var sliderOpen by remember { mutableStateOf(false) } var sliderOpen by remember { mutableStateOf(false) }
// ViewModel Initialisierung
val mapViewModel = remember { MapViewModel(application) }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = { AppTopBar() },
AppTopBar()
},
bottomBar = { bottomBar = {
BottomAppBar( BottomAppBar(
modifier = Modifier.height(120.dp), modifier = Modifier.height(120.dp),
containerColor = AppColor, containerColor = AppColor,
) { ) {
Row( IconButton(onClick = { sliderOpen = !sliderOpen; showReport = false }) {
modifier = Modifier Icon(Icons.Default.Menu, contentDescription = "Menu")
.fillMaxWidth()
.padding(start = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = {
sliderOpen = !sliderOpen
showReport = false
},
modifier = Modifier.padding(bottom = 8.dp)
) {
Icon(
Icons.Default.Menu,
contentDescription = "Menu",
)
}
Spacer(Modifier.weight(1f))
} }
} }
}, },
floatingActionButton = { floatingActionButton = {
LargeFloatingActionButton( LargeFloatingActionButton(
onClick = { onClick = { showReport = true; sliderOpen = false },
showReport = true
sliderOpen = false
},
modifier = Modifier.offset(y = 64.dp), modifier = Modifier.offset(y = 64.dp),
containerColor = ButtonColor containerColor = ButtonColor
) { ) { Icon(Icons.Default.Add, contentDescription = "Add") }
Icon(Icons.Default.Add, contentDescription = "Add")
}
}, },
floatingActionButtonPosition = FabPosition.Center, floatingActionButtonPosition = FabPosition.Center,
) { innerPadding -> ) { innerPadding ->
ContentScreen( ContentScreen(
modifier = Modifier.padding(innerPadding), modifier = Modifier.padding(innerPadding),
application, mapViewModel = mapViewModel,
showReport = showReport, showReport = showReport,
sliderOpen = sliderOpen, sliderOpen = sliderOpen,
onDismissReport = { showReport = false }) onDismissReport = { showReport = false })
@@ -139,54 +82,54 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
@Composable @Composable
fun ContentScreen( fun ContentScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
application: Application, mapViewModel: MapViewModel,
showReport: Boolean, showReport: Boolean,
sliderOpen: Boolean, sliderOpen: Boolean,
onDismissReport: () -> Unit onDismissReport: () -> Unit
) { ) {
val mapViewModel = remember { MapViewModel(application) }
val albumViewModel = remember { AlbumViewModel(Dispatchers.Default) } val albumViewModel = remember { AlbumViewModel(Dispatchers.Default) }
// ArcGIS Map erstellen
val map = remember {
createMap() //Funktion zur Erstellung der Map
}
//Standortbestimmung aus locationHelper.kt
val locationDisplay = setupLocationDisplay() val locationDisplay = setupLocationDisplay()
// VERBINDUNG ZUM VIEWMODEL (Hier wird das GPS-Werkzeug übergeben)
LaunchedEffect(locationDisplay) {
mapViewModel.locationDisplay = locationDisplay
}
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
// HINTERGRUND: Die Map
MapView( MapView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
arcGISMap = map, arcGISMap = mapViewModel.map,
mapViewProxy = mapViewModel.mapViewProxy,
locationDisplay = locationDisplay locationDisplay = locationDisplay
) )
// VORDERGRUND: Das Overlay (wenn showReport = true)
if (showReport) { if (showReport) {
ReportOverlay( ReportOverlay(
onCancel = onDismissReport, onCancel = onDismissReport,
onAdd = { /* später */ }, onAdd = { beschreibung, typ ->
mapViewModel.createFeatureAtCurrentLocation(
beschreibung = beschreibung,
typ = typ,
photos = albumViewModel.viewStateFlow.value.selectedPictures
)
onDismissReport()
},
viewModel = albumViewModel viewModel = albumViewModel
) )
} }
// RECHTSGRUND: Das Slider
SideSlider(visible = sliderOpen) { SideSlider(visible = sliderOpen) {
Text( SliderMenuItem(text = "Schäden filtern", icon = Icons.Default.FilterAlt, onClick = {})
"Menü", }
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 12.dp)
)
SliderMenuItem( if (mapViewModel.snackBarMessage.isNotEmpty()) {
text = "Schäden filtern", Snackbar(modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 80.dp)) {
icon = Icons.Default.FilterAlt, Text(mapViewModel.snackBarMessage)
onClick = { }
/* TODO */ LaunchedEffect(mapViewModel.snackBarMessage) {
} kotlinx.coroutines.delay(4000)
) mapViewModel.snackBarMessage = ""
}
} }
} }
} }
@@ -210,12 +153,17 @@ fun AppTopBar(
@Composable @Composable
fun ReportOverlay( fun ReportOverlay(
onCancel: () -> Unit, onCancel: () -> Unit,
onAdd: () -> Unit, onAdd: (beschreibung: String, typ: String) -> Unit,
viewModel: AlbumViewModel viewModel: AlbumViewModel
) { ) {
val viewState: AlbumViewState by viewModel.viewStateFlow.collectAsState() val viewState: AlbumViewState by viewModel.viewStateFlow.collectAsState()
val currentContext = LocalContext.current val currentContext = LocalContext.current
// State für Beschreibung und Typ
var beschreibung by remember { mutableStateOf("") }
var selectedTyp by remember { mutableStateOf("Schadenstyp wählen...") }
var dropdownExpanded by remember { mutableStateOf(false) }
// Launcher für Bildauswahl aus Galerie // Launcher für Bildauswahl aus Galerie
val pickImageFromAlbumLauncher = rememberLauncherForActivityResult( val pickImageFromAlbumLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(20) ActivityResultContracts.PickMultipleVisualMedia(20)
@@ -245,19 +193,13 @@ fun ReportOverlay(
} }
} }
// Funktion zum Starten der Kamera (prüft Berechtigung) // Funktion zum Starten der Kamera
fun startCamera() { fun startCamera() {
println("DEBUG: startCamera() aufgerufen")
val hasPermission = currentContext.checkSelfPermission(Manifest.permission.CAMERA) == val hasPermission = currentContext.checkSelfPermission(Manifest.permission.CAMERA) ==
android.content.pm.PackageManager.PERMISSION_GRANTED android.content.pm.PackageManager.PERMISSION_GRANTED
println("DEBUG: Hat Berechtigung? $hasPermission")
if (hasPermission) { if (hasPermission) {
println("DEBUG: Erstelle tempFileUrl")
// Berechtigung bereits erteilt -> direkt tempFileUrl erstellen
viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext)) viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
} else { } else {
println("DEBUG: Frage Berechtigung an")
// Berechtigung anfragen
permissionLauncher.launch(Manifest.permission.CAMERA) permissionLauncher.launch(Manifest.permission.CAMERA)
} }
} }
@@ -269,7 +211,6 @@ fun ReportOverlay(
} }
} }
// leichter Dim-Hintergrund
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -295,20 +236,39 @@ fun ReportOverlay(
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text( Text("Schadensbeschreibung:", color = Color.Black)
"Schadensbeschreibung:",
color = Color.Black
)
// Kamera-Buttons (über der weißen Box) // Typ-Auswahl (Dropdown)
Box {
OutlinedButton(
onClick = { dropdownExpanded = true },
modifier = Modifier.fillMaxWidth()
) {
Text("Typ: $selectedTyp")
}
DropdownMenu(
expanded = dropdownExpanded,
onDismissRequest = { dropdownExpanded = false }
) {
listOf("Straße", "Gehweg", "Fahrradweg", "Beleuchtung","Sonstiges").forEach { typ ->
DropdownMenuItem(
text = { Text(typ) },
onClick = {
selectedTyp = typ
dropdownExpanded = false
}
)
}
}
}
// Kamera-Buttons
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Button( Button(
onClick = { onClick = { startCamera() },
startCamera()
},
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text(text = "Foto aufnehmen") Text(text = "Foto aufnehmen")
@@ -325,15 +285,21 @@ fun ReportOverlay(
} }
} }
// Die weiße Textfeld Box // Textfeld für Beschreibung
Box( TextField(
value = beschreibung,
onValueChange = { beschreibung = it },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(220.dp) .height(220.dp),
.background(Color.White, RoundedCornerShape(12.dp)) placeholder = { Text("Beschreibung eingeben...") },
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.White,
unfocusedContainerColor = Color.White
)
) )
// Grid für ausgewählte Bilder (außerhalb der Box, aber in der Card) // Bilder-Grid
if (viewState.selectedPictures.isNotEmpty()) { if (viewState.selectedPictures.isNotEmpty()) {
Text( Text(
text = "Ausgewählte Bilder (${viewState.selectedPictures.size})", text = "Ausgewählte Bilder (${viewState.selectedPictures.size})",
@@ -357,6 +323,7 @@ fun ReportOverlay(
} }
} }
// Buttons
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
@@ -370,31 +337,22 @@ fun ReportOverlay(
disabledContentColor = Color.White disabledContentColor = Color.White
) )
) { ) {
Text( Text("Abbrechen", color = Color.Black)
"Abbrechen", }
color = Color.Black Button(
) onClick = { onAdd(beschreibung, selectedTyp) },
enabled = beschreibung.isNotBlank()
) {
Text("Hinzufügen")
} }
Button(onClick = onAdd) { Text("Hinzufügen") }
} }
} }
} }
} }
} }
fun createMap(): ArcGISMap { fun createMap(): ArcGISMap {
return ArcGISMap(BasemapStyle.ArcGISTopographic).apply { return ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
initialViewpoint = Viewpoint(53.14, 8.20, 20000.0)
initialViewpoint = Viewpoint(
53.14,
8.20,
20000.0)
} }
} }

View File

@@ -1,64 +1,182 @@
package com.example.snapandsolve
import android.app.Application import android.app.Application
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.arcgismaps.LoadStatus
import com.arcgismaps.data.ArcGISFeature import com.arcgismaps.data.ArcGISFeature
import com.arcgismaps.data.CodedValueDomain import com.arcgismaps.data.CodedValueDomain
import com.arcgismaps.data.ServiceFeatureTable import com.arcgismaps.data.ServiceFeatureTable
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.mapping.layers.FeatureLayer import com.arcgismaps.mapping.layers.FeatureLayer
import com.arcgismaps.mapping.view.LocationDisplay
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
class MapViewModel(application: Application ): AndroidViewModel(application) { class MapViewModel(application: Application) : AndroidViewModel(application) {
val map: ArcGISMap = ArcGISMap(BasemapStyle.OpenOsmStyle).apply { val map: ArcGISMap = ArcGISMap(BasemapStyle.OpenOsmStyle).apply {
initialViewpoint = Viewpoint(53.14, 8.20, 20000.0) initialViewpoint = Viewpoint(53.14, 8.20, 20000.0)
} }
/*
ALLES UNTER DIESEM KOMMENTAR WIRD NICHT GENUTZT. Aber EVENTUELL nötig zum einbinden von
Layer und Features.
*/
// Hold a reference to the selected feature.
var selectedFeature: ArcGISFeature? by mutableStateOf(null) var selectedFeature: ArcGISFeature? by mutableStateOf(null)
val mapViewProxy = MapViewProxy() val mapViewProxy = MapViewProxy()
//var currentFeatureOperation by mutableStateOf(FeatureOperationType.CREATE)
lateinit var featureLayer: FeatureLayer lateinit var featureLayer: FeatureLayer
// Create a snackbar message to display the result of feature operations.
var snackBarMessage: String by mutableStateOf("") var snackBarMessage: String by mutableStateOf("")
lateinit var serviceFeatureTable: ServiceFeatureTable lateinit var serviceFeatureTable: ServiceFeatureTable
var currentDamageType by mutableStateOf("") var currentDamageType by mutableStateOf("")
// The list of damage types to update the feature attribute.
var damageTypeList: List<String> = mutableListOf() var damageTypeList: List<String> = mutableListOf()
var locationDisplay: LocationDisplay? = null
init { init {
viewModelScope.launch { viewModelScope.launch {
serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/ArcGIS/rest/services/StrassenSchaeden/FeatureServer/0") serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/arcgis/rest/services/si_StrassenSchaeden/FeatureServer/0")
serviceFeatureTable.load().onSuccess { serviceFeatureTable.load().onSuccess {
// Get the field from the feature table that will be updated. val typeDamageField = serviceFeatureTable.fields.firstOrNull { it.name == "Typ" }
val typeDamageField = serviceFeatureTable.fields.first { it.name == "Typ" } val attributeDomain = typeDamageField?.domain as? CodedValueDomain
// Get the coded value domain for the field. attributeDomain?.codedValues?.forEach {
val attributeDomain = typeDamageField.domain as CodedValueDomain
// Add the damage types to the list.
attributeDomain.codedValues.forEach {
damageTypeList += it.name damageTypeList += it.name
} }
println("DEBUG: ServiceFeatureTable erfolgreich geladen")
}.onFailure {
println("DEBUG: Fehler beim Laden der Tabelle: ${it.message}")
} }
featureLayer = FeatureLayer.createWithFeatureTable(serviceFeatureTable) featureLayer = FeatureLayer.createWithFeatureTable(serviceFeatureTable)
map.operationalLayers.add(featureLayer) map.operationalLayers.add(featureLayer)
} }
} }
fun createFeatureAtCurrentLocation(
beschreibung: String,
typ: String,
photos: List<ImageBitmap> = emptyList()
) {
viewModelScope.launch {
println("DEBUG: createFeature gestartet") // Erscheint das in der Logcat?
try {
// 1. Location prüfen
val locationData = locationDisplay?.location?.value
val gpsPoint = locationData?.position
if (gpsPoint == null) {
println("DEBUG: Standort ist NULL - GPS Signal fehlt!")
snackBarMessage = "Kein GPS Signal. Bitte kurz warten oder nach draußen gehen."
return@launch
}
println("DEBUG: GPS gefunden: x=${gpsPoint.x}, y=${gpsPoint.y}")
// 2. Feature erstellen
// Wichtig: explizit WGS84, falls es nicht schon hat
val pointWgs84 = Point(gpsPoint.x, gpsPoint.y, SpatialReference.wgs84())
val feature = serviceFeatureTable.createFeature().apply {
geometry = pointWgs84
attributes["Typ"] = typ
attributes["Beschreibung"] = beschreibung
}
println("DEBUG: Feature lokal erstellt. Sende an Server...")
// 3. Hochladen
serviceFeatureTable.addFeature(feature).onSuccess {
println("DEBUG: addFeature erfolgreich")
applyEditsWithPhotos(feature as ArcGISFeature, photos)
}.onFailure {
println("DEBUG: addFeature FEHLER: ${it.message}")
snackBarMessage = "Fehler: ${it.message}"
}
} catch (e: Exception) {
println("DEBUG: CRASH in createFeature: ${e.message}")
e.printStackTrace()
snackBarMessage = "Fehler: ${e.message}"
}
}
}
private suspend fun applyEditsWithPhotos(feature: ArcGISFeature, photos: List<ImageBitmap>) {
serviceFeatureTable.applyEdits().onSuccess { editResults ->
val result = editResults.firstOrNull()
if (result != null && result.error == null) {
val serverObjectId = result.objectId
println("DEBUG: Server-Erfolg! Echte ObjectID: $serverObjectId")
if (photos.isNotEmpty()) {
// Fix: erstellen eine Abfrage für die neue ID
val queryParameters = com.arcgismaps.data.QueryParameters().apply {
objectIds.add(serverObjectId)
}
// laden das Feature neu vom Server
serviceFeatureTable.queryFeatures(queryParameters).onSuccess { queryResult ->
// Ein FeatureQueryResult ist ein Iterator, nehmen das erste Element
val fetchedFeature = queryResult.firstOrNull() as? ArcGISFeature
if (fetchedFeature != null) {
addPhotosToFeature(fetchedFeature, photos, serverObjectId)
} else {
println("DEBUG: Feature nach Query nicht gefunden")
snackBarMessage = "Fehler: Feature-ID $serverObjectId nicht gefunden."
}
}.onFailure {
println("DEBUG: Query fehlgeschlagen: ${it.message}")
snackBarMessage = "Fotos konnten nicht zugeordnet werden."
}
} else {
snackBarMessage = "Erfolgreich gemeldet! ID: $serverObjectId"
}
} else {
println("DEBUG: Server-Fehler bei applyEdits: ${result?.error?.message}")
snackBarMessage = "Serverfehler: ${result?.error?.message}"
}
}.onFailure {
println("DEBUG: applyEdits total fehlgeschlagen: ${it.message}")
snackBarMessage = "Senden fehlgeschlagen: ${it.message}"
}
}
private suspend fun addPhotosToFeature(
feature: ArcGISFeature,
photos: List<ImageBitmap>,
objectId: Long?
) {
println("DEBUG: Füge ${photos.size} Fotos hinzu...")
photos.forEachIndexed { index, imageBitmap ->
try {
val byteArray = imageBitmapToByteArray(imageBitmap)
feature.addAttachment(
name = "photo_$index.jpg",
contentType = "image/jpeg",
data = byteArray
)
} catch (e: Exception) {
println("DEBUG: Attachment-Fehler bei Foto $index: ${e.message}")
}
}
serviceFeatureTable.updateFeature(feature).onSuccess {
println("DEBUG: Feature mit Anhängen aktualisiert. Finales applyEdits...")
serviceFeatureTable.applyEdits().onSuccess {
snackBarMessage = "Gespeichert mit ${photos.size} Fotos! ID: $objectId"
}
}.onFailure {
println("DEBUG: updateFeature für Anhänge fehlgeschlagen: ${it.message}")
}
}
private fun imageBitmapToByteArray(imageBitmap: ImageBitmap): ByteArray {
val stream = ByteArrayOutputStream()
imageBitmap.asAndroidBitmap().compress(android.graphics.Bitmap.CompressFormat.JPEG, 80, stream)
return stream.toByteArray()
}
} }