Files
SnapAndSolve/app/src/main/java/com/example/snapandsolve/MapViewModel.kt
fr2651 a2866cc268 symbole angepasst
unused fun gelöscht
2026-02-14 10:54:36 +01:00

435 lines
18 KiB
Kotlin

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<List<DamageWithDistance>>(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<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/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<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.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<ImageBitmap> = emptyList(),
val point: Point? = null,
val status: String = "neu"
) {
val isValid: Boolean
get() =
beschreibung.isNotBlank() &&
typ != "Schadenstyp wählen..." &&
point != null &&
photos.isNotEmpty()
}