- Schadensliste wurde implementiert
This commit is contained in:
6
.idea/appInsightsSettings.xml
generated
Normal file
6
.idea/appInsightsSettings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AppInsightsSettings">
|
||||||
|
<option name="selectedTabId" value="Android Vitals" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/deploymentTargetSelector.xml
generated
12
.idea/deploymentTargetSelector.xml
generated
@@ -4,14 +4,22 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2025-12-21T13:16:32.335138100Z">
|
<DropdownSelection timestamp="2026-01-25T17:20:21.835673100Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\sklau\.android\avd\Medium_Phone.avd" />
|
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\sklau\.android\avd\Medium_Phone.avd" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
<DialogSelection />
|
<DialogSelection>
|
||||||
|
<targets>
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\sklau\.android\avd\Medium_Phone.avd" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</targets>
|
||||||
|
</DialogSelection>
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/markdown.xml
generated
Normal file
8
.idea/markdown.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MarkdownSettings">
|
||||||
|
<option name="previewPanelProviderInfo">
|
||||||
|
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -133,6 +133,8 @@ fun ContentScreen(
|
|||||||
// NEU: State für Filter-Dialog
|
// NEU: State für Filter-Dialog
|
||||||
var showFilterDialog by remember { mutableStateOf(false) }
|
var showFilterDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var showDamageList by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
// Map
|
// Map
|
||||||
MapSegment(
|
MapSegment(
|
||||||
@@ -140,6 +142,18 @@ fun ContentScreen(
|
|||||||
mapViewModel = mapViewModel
|
mapViewModel = mapViewModel
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Dialog
|
||||||
|
if (showDamageList) {
|
||||||
|
DamageListDialog(
|
||||||
|
onDismiss = { showDamageList = false },
|
||||||
|
onDamageClick = { feature ->
|
||||||
|
mapViewModel.selectedFeature = feature
|
||||||
|
mapViewModel.showFeatureInfo = true
|
||||||
|
},
|
||||||
|
mapViewModel = mapViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Report Overlay
|
// Report Overlay
|
||||||
if (showReport) {
|
if (showReport) {
|
||||||
ReportOverlay(
|
ReportOverlay(
|
||||||
@@ -196,7 +210,7 @@ fun ContentScreen(
|
|||||||
SliderMenuItem(
|
SliderMenuItem(
|
||||||
text = "Schadensliste",
|
text = "Schadensliste",
|
||||||
icon = Icons.Default.FormatListNumbered,
|
icon = Icons.Default.FormatListNumbered,
|
||||||
onClick = { /* TODO */ }
|
onClick = { showDamageList = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
var reopenReport by mutableStateOf(false)
|
var reopenReport by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
var showFeatureInfo by mutableStateOf(false)
|
var showFeatureInfo by mutableStateOf(false)
|
||||||
private set
|
public set
|
||||||
var selectedFeature: ArcGISFeature? by mutableStateOf(null)
|
var selectedFeature: ArcGISFeature? by mutableStateOf(null)
|
||||||
val mapViewProxy = MapViewProxy()
|
val mapViewProxy = MapViewProxy()
|
||||||
var reportDraft by mutableStateOf(ReportDraft())
|
var reportDraft by mutableStateOf(ReportDraft())
|
||||||
|
|||||||
@@ -0,0 +1,551 @@
|
|||||||
|
package com.example.snapandsolve.ui.theme
|
||||||
|
|
||||||
|
import MapViewModel
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Image
|
||||||
|
import androidx.compose.material.icons.filled.LocationOn
|
||||||
|
import androidx.compose.material.icons.filled.MyLocation
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.arcgismaps.data.ArcGISFeature
|
||||||
|
import com.arcgismaps.data.QueryParameters
|
||||||
|
import com.arcgismaps.geometry.Point
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class für einen Schaden mit Entfernung und Foto
|
||||||
|
*/
|
||||||
|
data class DamageWithDistance(
|
||||||
|
val feature: ArcGISFeature,
|
||||||
|
val distanceInMeters: Double,
|
||||||
|
val typ: String,
|
||||||
|
val beschreibung: String,
|
||||||
|
val objectId: Long,
|
||||||
|
val rating: Int,
|
||||||
|
val photo: ImageBitmap? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet die Entfernung zwischen zwei Koordinaten mit Haversine-Formel
|
||||||
|
*/
|
||||||
|
fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
|
||||||
|
val R = 6371000.0 // Erdradius in Metern
|
||||||
|
|
||||||
|
val dLat = Math.toRadians(lat2 - lat1)
|
||||||
|
val dLon = Math.toRadians(lon2 - lon1)
|
||||||
|
|
||||||
|
val a = sin(dLat / 2) * sin(dLat / 2) +
|
||||||
|
cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
|
||||||
|
sin(dLon / 2) * sin(dLon / 2)
|
||||||
|
|
||||||
|
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||||
|
|
||||||
|
return R * c
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog für Schadensliste mit Entfernungsfilter
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DamageListDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onDamageClick: (ArcGISFeature) -> Unit,
|
||||||
|
mapViewModel: MapViewModel
|
||||||
|
) {
|
||||||
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
|
||||||
|
var damages by remember { mutableStateOf<List<DamageWithDistance>>(emptyList()) }
|
||||||
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
|
var maxDistance by remember { mutableStateOf(1000f) }
|
||||||
|
var userLocation by remember { mutableStateOf<Point?>(null) }
|
||||||
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(maxDistance) {
|
||||||
|
isLoading = true
|
||||||
|
errorMessage = null
|
||||||
|
|
||||||
|
val location = mapViewModel.locationDisplay?.location?.value?.position
|
||||||
|
|
||||||
|
if (location == null) {
|
||||||
|
errorMessage = "Standort nicht verfügbar. Bitte GPS aktivieren."
|
||||||
|
isLoading = false
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
userLocation = location
|
||||||
|
|
||||||
|
val result = mapViewModel.loadDamagesNearby(location, maxDistance.toDouble(), context)
|
||||||
|
damages = result
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.9f),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(20.dp)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Schäden in der Nähe",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Schließen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 12.dp))
|
||||||
|
|
||||||
|
// Entfernungs-Slider
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.MyLocation,
|
||||||
|
contentDescription = "Umkreis",
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Umkreis: ${if (maxDistance >= 1000) "${(maxDistance / 1000).roundToInt()} km" else "${maxDistance.toInt()} m"}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value = maxDistance,
|
||||||
|
onValueChange = { maxDistance = it },
|
||||||
|
valueRange = 100f..5000f,
|
||||||
|
steps = 48,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text("100 m", style = MaterialTheme.typography.bodySmall)
|
||||||
|
Text("5 km", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
if (!isLoading && errorMessage == null) {
|
||||||
|
Text(
|
||||||
|
text = "${damages.size} Schäden gefunden",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
|
||||||
|
// Content
|
||||||
|
when {
|
||||||
|
isLoading -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text("Lade Schäden...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage != null -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "⚠️",
|
||||||
|
style = MaterialTheme.typography.displayLarge
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = errorMessage!!,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
damages.isEmpty() -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "🔍",
|
||||||
|
style = MaterialTheme.typography.displayLarge
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = "Keine Schäden im Umkreis",
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(damages) { damage ->
|
||||||
|
DamageListItemWithPhoto(
|
||||||
|
damage = damage,
|
||||||
|
onClick = {
|
||||||
|
onDamageClick(damage.feature)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einzelnes Schadens-Item in der Liste MIT FOTO
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DamageListItemWithPhoto(
|
||||||
|
damage: DamageWithDistance,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(100.dp)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// LINKS: Foto
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(100.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.clip(RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp))
|
||||||
|
) {
|
||||||
|
if (damage.photo != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = damage.photo,
|
||||||
|
contentDescription = "Schadensfoto",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text("📷", style = MaterialTheme.typography.headlineLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RECHTS: Info
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f)
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
// Typ mit Emoji
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = getEmojiForType(damage.typ),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = damage.typ,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beschreibung
|
||||||
|
Text(
|
||||||
|
text = damage.beschreibung.take(40) + if (damage.beschreibung.length > 40) "..." else "",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info-Row
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text("📍", style = MaterialTheme.typography.labelSmall)
|
||||||
|
Text(
|
||||||
|
text = formatDistance(damage.distanceInMeters),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (damage.rating > 0) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text("👥", style = MaterialTheme.typography.labelSmall)
|
||||||
|
Text(
|
||||||
|
text = "${damage.rating}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension-Funktion für MapViewModel
|
||||||
|
* Lädt alle Schäden im Umkreis
|
||||||
|
*/
|
||||||
|
suspend fun MapViewModel.loadDamagesNearby(
|
||||||
|
userLocation: Point,
|
||||||
|
maxDistanceMeters: Double,
|
||||||
|
context: Context
|
||||||
|
): List<DamageWithDistance> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
println("DEBUG 1: Lade Schäden im Umkreis von ${maxDistanceMeters}m")
|
||||||
|
println("DEBUG 1.1: serviceFeatureTable = $serviceFeatureTable")
|
||||||
|
|
||||||
|
val queryParams = QueryParameters().apply {
|
||||||
|
whereClause = "1=1"
|
||||||
|
}
|
||||||
|
|
||||||
|
println("DEBUG 2: QueryParameters erstellt")
|
||||||
|
val queryResult = serviceFeatureTable?.queryFeatures(queryParams)?.getOrNull()
|
||||||
|
|
||||||
|
println("DEBUG 3: queryResult = $queryResult")
|
||||||
|
|
||||||
|
if (queryResult == null) {
|
||||||
|
println("DEBUG ERROR: queryResult ist NULL!")
|
||||||
|
return@withContext emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val resultList = queryResult.toList()
|
||||||
|
println("DEBUG 3.1: resultList.size = ${resultList.size}")
|
||||||
|
|
||||||
|
if (resultList.isEmpty()) {
|
||||||
|
println("DEBUG: queryResult ist LEER (0 Features)")
|
||||||
|
return@withContext emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val damagesWithDistance = mutableListOf<DamageWithDistance>()
|
||||||
|
var processedCount = 0
|
||||||
|
var addedCount = 0
|
||||||
|
|
||||||
|
queryResult.forEach { geoElement ->
|
||||||
|
processedCount++
|
||||||
|
println("DEBUG 4.$processedCount: Verarbeite Feature...")
|
||||||
|
|
||||||
|
val feature = geoElement as? ArcGISFeature
|
||||||
|
if (feature == null) {
|
||||||
|
println("DEBUG 4.$processedCount: Feature ist NULL, überspringe")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
feature.load().getOrNull()
|
||||||
|
|
||||||
|
val featureGeometry = feature.geometry as? Point
|
||||||
|
if (featureGeometry == null) {
|
||||||
|
println("DEBUG 4.$processedCount: Geometrie ist NULL")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transformiere Feature-Koordinaten in die gleiche räumliche Referenz wie userLocation
|
||||||
|
val targetSpatialRef = userLocation.spatialReference
|
||||||
|
if (targetSpatialRef == null) {
|
||||||
|
println("DEBUG 4.$processedCount: User SpatialReference ist NULL")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
val transformedGeometry = com.arcgismaps.geometry.GeometryEngine.projectOrNull(
|
||||||
|
featureGeometry,
|
||||||
|
targetSpatialRef
|
||||||
|
) as? Point
|
||||||
|
|
||||||
|
if (transformedGeometry == null) {
|
||||||
|
println("DEBUG 4.$processedCount: Transformation fehlgeschlagen")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entfernung berechnen mit Haversine-Formel
|
||||||
|
val distance = calculateDistance(
|
||||||
|
userLocation.y,
|
||||||
|
userLocation.x,
|
||||||
|
transformedGeometry.y,
|
||||||
|
transformedGeometry.x
|
||||||
|
)
|
||||||
|
|
||||||
|
println("DEBUG 4.$processedCount: Distance = $distance, max = $maxDistanceMeters")
|
||||||
|
|
||||||
|
if (distance <= maxDistanceMeters) {
|
||||||
|
addedCount++
|
||||||
|
val typ = feature.attributes["Typ"]?.toString() ?: "Unbekannt"
|
||||||
|
val beschreibung = feature.attributes["Beschreibung"]?.toString() ?: "Keine Beschreibung"
|
||||||
|
val objectId = (feature.attributes["OBJECTID"] as? Number)?.toLong() ?: 0L
|
||||||
|
val rating = (feature.attributes["communitycounter"] as? Number)?.toInt() ?: 0
|
||||||
|
|
||||||
|
// Foto laden (async)
|
||||||
|
val photo = try {
|
||||||
|
val attachments = feature.fetchAttachments().getOrNull()
|
||||||
|
val firstAttachment = attachments?.firstOrNull()
|
||||||
|
if (firstAttachment != null) {
|
||||||
|
val data = firstAttachment.fetchData().getOrNull()
|
||||||
|
if (data != null) {
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
|
||||||
|
bitmap?.asImageBitmap()
|
||||||
|
} else null
|
||||||
|
} else null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
damagesWithDistance.add(
|
||||||
|
DamageWithDistance(
|
||||||
|
feature = feature,
|
||||||
|
distanceInMeters = distance,
|
||||||
|
typ = typ,
|
||||||
|
beschreibung = beschreibung,
|
||||||
|
objectId = objectId,
|
||||||
|
rating = rating,
|
||||||
|
photo = photo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sorted = damagesWithDistance.sortedBy { it.distanceInMeters }
|
||||||
|
println("DEBUG: $processedCount Features verarbeitet, $addedCount hinzugefügt")
|
||||||
|
sorted
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("DEBUG ERROR: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(context, "Fehler: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatDistance(meters: Double): String {
|
||||||
|
return if (meters < 1000) {
|
||||||
|
"${meters.roundToInt()} m"
|
||||||
|
} else {
|
||||||
|
"${"%.1f".format(meters / 1000)} km"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEmojiForType(typ: String): String {
|
||||||
|
return when (typ) {
|
||||||
|
"Straße" -> "🛣️"
|
||||||
|
"Gehweg" -> "🚶"
|
||||||
|
"Fahrradweg" -> "🚴"
|
||||||
|
"Beleuchtung" -> "💡"
|
||||||
|
"Sonstiges" -> "📍"
|
||||||
|
else -> "•"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
docs/DamageListSystem.md
Normal file
0
docs/DamageListSystem.md
Normal file
Reference in New Issue
Block a user