Files
SnapAndSolve/app/src/main/java/com/example/snapandsolve/MapViewModel.kt
si2503 05426b687c - Bewertungsystem
- Filtern nach Schäden
- Docs Ordner mit Markdown dummies gefüllt.
2026-01-21 21:37:58 +01:00

409 lines
17 KiB
Kotlin

import android.app.Application
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 kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
class MapViewModel(application: Application) : 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 selectedOperation by mutableStateOf(FeatureOperationType.DEFAULT)
var reopenReport by mutableStateOf(false)
private set
var showFeatureInfo by mutableStateOf(false)
private 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<String> = 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/si_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")
}.onFailure {
println("DEBUG: Fehler beim Laden der Tabelle: ${it.message}")
}
featureLayer = FeatureLayer.createWithFeatureTable(serviceFeatureTable)
map.operationalLayers.add(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<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 = 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(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.DELETE -> deleteFeatureAt(singleTapConfirmedEvent.screenCoordinate)
FeatureOperationType.UPDATE_ATTRIBUTE -> selectFeatureForAttributeEditAt(singleTapConfirmedEvent.screenCoordinate)
FeatureOperationType.UPDATE_GEOMETRY -> updateFeatureGeometryAt(singleTapConfirmedEvent.screenCoordinate)
FeatureOperationType.PICK_REPORT_LOCATION -> pickReportLocation(singleTapConfirmedEvent.screenCoordinate)
// RATE_FEATURE wird nicht mehr gebraucht - wird im DEFAULT mit behandelt!
}
}
private fun deleteFeatureAt(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 ->
selectedFeature = (identifyResult.geoElements.firstOrNull() as? ArcGISFeature)?.also {
featureLayer.selectFeature(it)
}
}
}
}
}
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
}
// 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()
}
}
enum class FeatureOperationType(val operationName: String, val instruction: String) {
DEFAULT("Default", ""),
DELETE("Delete feature", "Select an existing feature to delete it."),
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<ImageBitmap> = emptyList(),
val point: Point? = null
) {
val isValid: Boolean
get() =
beschreibung.isNotBlank() &&
typ != "Schadenstyp wählen..." &&
point != null &&
photos.isNotEmpty()
}