import android.app.Application import android.content.Context import android.graphics.Bitmap import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.unit.dp import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.arcgismaps.LoadStatus import com.arcgismaps.data.ArcGISFeature import com.arcgismaps.data.CodedValueDomain import com.arcgismaps.data.QueryParameters 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.BasemapStyle import com.arcgismaps.mapping.Viewpoint import com.arcgismaps.mapping.layers.FeatureLayer import com.arcgismaps.mapping.symbology.SimpleMarkerSymbol import com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyle import com.arcgismaps.mapping.view.Graphic import com.arcgismaps.mapping.view.GraphicsOverlay import com.arcgismaps.mapping.view.LocationDisplay import com.arcgismaps.mapping.view.ScreenCoordinate import com.arcgismaps.mapping.view.SingleTapConfirmedEvent import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy import com.example.snapandsolve.view.createTypStatusRenderer import com.example.snapandsolve.viewmodel.findNearbyDamageOfSameType import kotlinx.coroutines.launch import java.io.ByteArrayOutputStream class MapViewModel(application: Application, context: Context) : AndroidViewModel(application) { companion object { // Zentrale Definition der Schadenstypen val DAMAGE_TYPES = listOf( "Straße", "Gehweg", "Fahrradweg", "Beleuchtung", "Sonstiges" ) } val map: ArcGISMap = ArcGISMap(BasemapStyle.OpenOsmStyle).apply { initialViewpoint = Viewpoint(53.14, 8.20, 20000.0) } var duplicateDamages by mutableStateOf>(emptyList()) private set var selectedOperation by mutableStateOf(FeatureOperationType.DEFAULT) var reopenReport by mutableStateOf(false) private set var showFeatureInfo by mutableStateOf(false) public set var selectedFeature: ArcGISFeature? by mutableStateOf(null) val mapViewProxy = MapViewProxy() var reportDraft by mutableStateOf(ReportDraft()) private set lateinit var featureLayer: FeatureLayer var snackBarMessage: String by mutableStateOf("") lateinit var serviceFeatureTable: ServiceFeatureTable var currentDamageType by mutableStateOf("") var damageTypeList: List = mutableListOf() var locationDisplay: LocationDisplay? = null // Create a red circle simple marker symbol. val redCircleSymbol = SimpleMarkerSymbol( style = SimpleMarkerSymbolStyle.Circle, color = com.arcgismaps.Color.red, size = 10.0f ) var pointGraphic = Graphic(null, redCircleSymbol) val tempOverlay = GraphicsOverlay() init { tempOverlay.graphics.add(pointGraphic) viewModelScope.launch { serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/ArcGIS/rest/services/251120_StrassenSchaeden/FeatureServer/0") serviceFeatureTable.load().onSuccess { val typeDamageField = serviceFeatureTable.fields.firstOrNull { it.name == "Typ" } val attributeDomain = typeDamageField?.domain as? CodedValueDomain attributeDomain?.codedValues?.forEach { damageTypeList += it.name } println("DEBUG: ServiceFeatureTable erfolgreich geladen") // ===== DEBUG: Alle verfügbaren Felder ausgeben ===== println("DEBUG: Verfügbare Felder in ServiceFeatureTable:") serviceFeatureTable.fields.forEach { field -> println(" - ${field.name}") } println("DEBUG: Ende Feldliste") }.onFailure { println("DEBUG: Fehler beim Laden der Tabelle: ${it.message}") } featureLayer = FeatureLayer.createWithFeatureTable(serviceFeatureTable) featureLayer.renderer = createTypStatusRenderer(context) map.operationalLayers.add(featureLayer) // ===== DEBUG: Felder nach dem Hinzufügen zur Map ===== featureLayer.load().onSuccess { println("DEBUG: FeatureLayer erfolgreich geladen") val table = featureLayer.featureTable if (table != null) { println("DEBUG: Verfügbare Felder im FeatureLayer:") table.fields.forEach { field -> println(" - ${field.name}") } println("DEBUG: Ende Feldliste FeatureLayer") } } } } fun pickCurrentLocation() { // keine Coroutine nötig, das ist alles sync val pos = locationDisplay?.location?.value?.position if (pos == null) { snackBarMessage = "Kein GPS Signal. Bitte kurz warten oder Standort aktivieren." return } // pos ist ggf. schon Point, aber wir erzwingen WGS84 (sicher für deinen Feature-Service) val pointWgs84 = if (pos.spatialReference == SpatialReference.wgs84()) pos else GeometryEngine.projectOrNull(pos, SpatialReference.wgs84()) as Point updateReportDraft { copy(point = pointWgs84) } pointGraphic.geometry = pointWgs84 snackBarMessage = "Position aus GPS gesetzt." } private suspend fun applyEditsWithPhotos(feature: ArcGISFeature, photos: List) { 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 = 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, 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(Bitmap.CompressFormat.JPEG, 80, stream) return stream.toByteArray() } fun onTap(singleTapConfirmedEvent: SingleTapConfirmedEvent) { if (featureLayer.loadStatus.value != LoadStatus.Loaded) { snackBarMessage = "Layer not loaded!" return } when (selectedOperation) { FeatureOperationType.DEFAULT -> selectFeatureAt(singleTapConfirmedEvent.screenCoordinate) FeatureOperationType.UPDATE_ATTRIBUTE -> selectFeatureForAttributeEditAt(singleTapConfirmedEvent.screenCoordinate) FeatureOperationType.UPDATE_GEOMETRY -> updateFeatureGeometryAt(singleTapConfirmedEvent.screenCoordinate) FeatureOperationType.PICK_REPORT_LOCATION -> pickReportLocation(singleTapConfirmedEvent.screenCoordinate) } } private fun selectFeatureForAttributeEditAt(screenCoordinate: ScreenCoordinate) { featureLayer?.let { featureLayer -> // Clear any existing selection. featureLayer.clearSelection() selectedFeature = null viewModelScope.launch { // Determine if a user tapped on a feature. mapViewProxy.identify(featureLayer, screenCoordinate, 10.dp).onSuccess { identifyResult -> // Get the identified feature. val identifiedFeature = identifyResult.geoElements.firstOrNull() as? ArcGISFeature identifiedFeature?.let { val currentAttributeValue = it.attributes["typ"] as String currentDamageType = currentAttributeValue selectedFeature = it.also { featureLayer.selectFeature(it) } } ?: run { // Reset damage type if no feature identified. currentDamageType = "" } } } } } private fun updateFeatureGeometryAt(screenCoordinate: ScreenCoordinate) { featureLayer?.let { featureLayer -> when (selectedFeature) { // When no feature is selected. null -> { viewModelScope.launch { // Determine if a user tapped on a feature. mapViewProxy.identify(featureLayer, screenCoordinate, 10.dp).onSuccess { identifyResult -> // Get the identified feature. val identifiedFeature = identifyResult.geoElements.firstOrNull() as? ArcGISFeature identifiedFeature?.let { selectedFeature = it.also { featureLayer.selectFeature(it) } } } } } else -> { mapViewProxy.screenToLocationOrNull(screenCoordinate)?.let { mapPoint -> // Normalize the point - needed when the tapped location is over the international date line. val destinationPoint = GeometryEngine.normalizeCentralMeridian(mapPoint) viewModelScope.launch { selectedFeature?.let { selectedFeature -> // Load the feature. selectedFeature.load().onSuccess { // Update the geometry of the selected feature. selectedFeature.geometry = destinationPoint // Apply the edit to the feature table. serviceFeatureTable?.updateFeature(selectedFeature) // Push the update to the service with the service geodatabase. serviceFeatureTable?.applyEdits()?.onSuccess { snackBarMessage = "Moved feature ${selectedFeature.attributes["objectid"]}" }?.onFailure { snackBarMessage = "Failed to move feature ${selectedFeature.attributes["objectid"]}" } } } } } } } } } private fun pickReportLocation(screen: ScreenCoordinate) { val mapPoint = mapViewProxy.screenToLocationOrNull(screen) val p = mapPoint?.let { GeometryEngine.normalizeCentralMeridian(it) as? Point } if (p != null) { reportDraft = reportDraft.copy(point = p) pointGraphic.geometry = p reopenReport = true snackBarMessage = "Position gesetzt." } else { snackBarMessage = "Position konnte nicht gesetzt werden." } selectedOperation = FeatureOperationType.DEFAULT } fun startPickReportLocation() { selectedOperation = FeatureOperationType.PICK_REPORT_LOCATION snackBarMessage = "Tippe auf die Karte, um die Position zu setzen." } fun consumeReopenReport() { reopenReport = false } fun resetDraft() { reportDraft = ReportDraft() pointGraphic.geometry = null selectedOperation = FeatureOperationType.DEFAULT } fun updateReportDraft(update: ReportDraft.() -> ReportDraft) { reportDraft = reportDraft.update() } fun submitDraftToLayer() { val draft = reportDraft if (!draft.isValid) { snackBarMessage = "Bitte Beschreibung, Typ, Position und Fotos setzen." return } viewModelScope.launch { try { // 1) Feature lokal erstellen val feature = serviceFeatureTable.createFeature().apply { geometry = draft.point attributes["Beschreibung"] = draft.beschreibung attributes["Typ"] = draft.typ attributes["status"] = draft.status } // 2) Erst addFeature + applyEdits => Feature existiert am Server (ObjectID) serviceFeatureTable.addFeature(feature).onSuccess { // applyEditsWithPhotos macht bei dir: applyEdits -> ObjectID holen -> feature neu queryn -> attachments adden applyEditsWithPhotos(feature as ArcGISFeature, draft.photos) // Draft/Preview zurücksetzen (am besten nach Erfolg; fürs Erste hier ok) resetDraft() }.onFailure { snackBarMessage = "Fehler beim Hinzufügen: ${it.message}" } } catch (e: Exception) { snackBarMessage = "Fehler: ${e.message}" } } } fun selectFeatureAt(screenCoordinate: ScreenCoordinate) { featureLayer?.let { featureLayer -> // Clear any existing selection. featureLayer.clearSelection() selectedFeature = null viewModelScope.launch { // Determine if a user tapped on a feature. mapViewProxy.identify(featureLayer, screenCoordinate, 10.dp).onSuccess { identifyResult -> // Get the identified feature. val identifiedFeature = identifyResult.geoElements.firstOrNull() as? ArcGISFeature identifiedFeature?.let { selectedFeature = it.also { featureLayer.selectFeature(it) showFeatureInfo = true } } } } } } fun closeFeatureInfo() { showFeatureInfo = false selectedFeature = null featureLayer.clearSelection() } suspend fun isDuplicateNearby(radiusMeters: Double): Boolean { val p = reportDraft.point ?: return false val t = reportDraft.typ duplicateDamages = findNearbyDamageOfSameType(serviceFeatureTable, p, t, radiusMeters) return duplicateDamages.isNotEmpty() } fun clearDuplicateDamages() { duplicateDamages = emptyList() } fun getFeatureTable(): ServiceFeatureTable? { return featureLayer?.featureTable as? ServiceFeatureTable } } enum class FeatureOperationType(val operationName: String, val instruction: String) { DEFAULT("Default", ""), UPDATE_ATTRIBUTE("Update attribute", "Select an existing feature to edit its attribute."), UPDATE_GEOMETRY("Update geometry", "Select an existing feature and tap the map to move it to a new position."), PICK_REPORT_LOCATION("Pick report location", "Tippe auf die Karte, um die Position zu setzen."), } data class ReportDraft( val beschreibung: String = "", val typ: String = "Schadenstyp wählen...", val photos: List = emptyList(), val point: Point? = null, val status: String = "neu" ) { val isValid: Boolean get() = beschreibung.isNotBlank() && typ != "Schadenstyp wählen..." && point != null && photos.isNotEmpty() }