Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -8,12 +8,12 @@ import androidx.compose.ui.graphics.ImageBitmap
|
|||||||
*/
|
*/
|
||||||
data class AlbumViewState(
|
data class AlbumViewState(
|
||||||
/**
|
/**
|
||||||
* holds the URL of the temporary file which stores the image taken by the camera.
|
* Speichert die URL der temporären Datei des Kamerabildes.
|
||||||
*/
|
*/
|
||||||
val tempFileUrl: Uri? = null,
|
val tempFileUrl: Uri? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* holds the list of images taken by camera or selected pictures from the gallery.
|
* Speichert eine Liste der Bilder, die entweder über die Kamera aufgenommen oder aus der Galerie ausgewählt wurden.
|
||||||
*/
|
*/
|
||||||
val selectedPictures: List<ImageBitmap> = emptyList(),
|
val selectedPictures: List<ImageBitmap> = emptyList(),
|
||||||
)
|
)
|
||||||
@@ -167,7 +167,7 @@ class ProximityNotificationService : Service() {
|
|||||||
|
|
||||||
Log.d("ProximityService", "Feature point SR: ${featureGeometry.spatialReference?.wkid}")
|
Log.d("ProximityService", "Feature point SR: ${featureGeometry.spatialReference?.wkid}")
|
||||||
|
|
||||||
// KRITISCHER FIX: Beide Punkte ins gleiche Koordinatensystem bringen
|
// Beide Punkte ins gleiche Koordinatensystem bringen von UTM ins WGS
|
||||||
val projectedCurrentPoint = if (currentPoint.spatialReference?.wkid != featureGeometry.spatialReference?.wkid) {
|
val projectedCurrentPoint = if (currentPoint.spatialReference?.wkid != featureGeometry.spatialReference?.wkid) {
|
||||||
GeometryEngine.projectOrNull(currentPoint, featureGeometry.spatialReference!!) as? Point
|
GeometryEngine.projectOrNull(currentPoint, featureGeometry.spatialReference!!) as? Point
|
||||||
} else {
|
} else {
|
||||||
@@ -179,7 +179,7 @@ class ProximityNotificationService : Service() {
|
|||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
// KORRIGIERTE Distanzberechnung mit GeometryEngine
|
// korrigierte Distanzberechnung mit GeometryEngine
|
||||||
val distanceResult = GeometryEngine.distanceGeodeticOrNull(
|
val distanceResult = GeometryEngine.distanceGeodeticOrNull(
|
||||||
point1 = projectedCurrentPoint,
|
point1 = projectedCurrentPoint,
|
||||||
point2 = featureGeometry,
|
point2 = featureGeometry,
|
||||||
@@ -214,14 +214,14 @@ class ProximityNotificationService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun sendDamageNotification(feature: ArcGISFeature, distance: Double) {
|
private fun sendDamageNotification(feature: ArcGISFeature, distance: Double) {
|
||||||
// Versuche verschiedene Attribut-Namen für die Kategorie
|
|
||||||
val kategorie = feature.attributes["Kategorie"]?.toString()
|
val kategorie = feature.attributes["Kategorie"]?.toString()
|
||||||
?: feature.attributes["kategorie"]?.toString()
|
?: feature.attributes["kategorie"]?.toString()
|
||||||
?: feature.attributes["Category"]?.toString()
|
?: feature.attributes["Category"]?.toString()
|
||||||
?: feature.attributes["category"]?.toString()
|
?: feature.attributes["category"]?.toString()
|
||||||
?: "Straßenschaden" // Fallback
|
?: "Straßenschaden" // Fallback
|
||||||
|
|
||||||
// Versuche verschiedene Attribut-Namen für die Beschreibung
|
|
||||||
val beschreibung = feature.attributes["Beschreibung"]?.toString()
|
val beschreibung = feature.attributes["Beschreibung"]?.toString()
|
||||||
?: feature.attributes["beschreibung"]?.toString()
|
?: feature.attributes["beschreibung"]?.toString()
|
||||||
?: feature.attributes["Description"]?.toString()
|
?: feature.attributes["Description"]?.toString()
|
||||||
@@ -229,7 +229,7 @@ class ProximityNotificationService : Service() {
|
|||||||
|
|
||||||
val distanceText = String.format("%.0f", distance)
|
val distanceText = String.format("%.0f", distance)
|
||||||
|
|
||||||
// Baue den Notification-Text
|
// Notification-Text
|
||||||
val notificationText = if (beschreibung != null && beschreibung.isNotEmpty()) {
|
val notificationText = if (beschreibung != null && beschreibung.isNotEmpty()) {
|
||||||
"$kategorie: $beschreibung - ca. ${distanceText}m entfernt"
|
"$kategorie: $beschreibung - ca. ${distanceText}m entfernt"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ fun DamageFilterDialog(
|
|||||||
// Status-Liste
|
// Status-Liste
|
||||||
val statusTypes = listOf("neu", "in Bearbeitung", "Schaden behoben")
|
val statusTypes = listOf("neu", "in Bearbeitung", "Schaden behoben")
|
||||||
|
|
||||||
// WICHTIG: Wenn keine Filter aktiv sind, alle Typen standardmäßig auswählen!
|
|
||||||
var selectedFilters by remember {
|
var selectedFilters by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
if (currentFilters.isEmpty()) damageTypes.toSet() else currentFilters
|
if (currentFilters.isEmpty()) damageTypes.toSet() else currentFilters
|
||||||
@@ -120,7 +120,7 @@ fun DamageFilterDialog(
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
// ===== TYP-FILTER =====
|
// Filtertyp
|
||||||
Text(
|
Text(
|
||||||
text = "Schadenstypen:",
|
text = "Schadenstypen:",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
@@ -153,7 +153,7 @@ fun DamageFilterDialog(
|
|||||||
|
|
||||||
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
|
||||||
// ===== STATUS-FILTER =====
|
// Filter nach Status
|
||||||
Text(
|
Text(
|
||||||
text = "Bearbeitungsstatus:",
|
text = "Bearbeitungsstatus:",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
@@ -190,7 +190,7 @@ fun DamageFilterDialog(
|
|||||||
|
|
||||||
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
|
||||||
// ===== DATUMS-FILTER =====
|
// Filter nach Datum
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -364,8 +364,8 @@ fun DamageFilterDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension-Funktion für MapViewModel
|
* Erweiterungs-Funktion für MapViewModel
|
||||||
* Wendet Filter auf den FeatureLayer an (Typ + Status + Datum - UNABHÄNGIG)
|
* Wendet Filter auf den FeatureLayer an
|
||||||
*/
|
*/
|
||||||
suspend fun MapViewModel.applyDamageFilter(
|
suspend fun MapViewModel.applyDamageFilter(
|
||||||
selectedTypes: Set<String>,
|
selectedTypes: Set<String>,
|
||||||
@@ -379,22 +379,22 @@ suspend fun MapViewModel.applyDamageFilter(
|
|||||||
try {
|
try {
|
||||||
val whereClauses = mutableListOf<String>()
|
val whereClauses = mutableListOf<String>()
|
||||||
|
|
||||||
// ===== TYP-FILTER =====
|
// Filter Typ
|
||||||
// WICHTIG: Nur hinzufügen wenn nicht ALLE Typen ausgewählt sind
|
|
||||||
if (selectedTypes.isNotEmpty() && selectedTypes.size < DAMAGE_TYPES.size) {
|
if (selectedTypes.isNotEmpty() && selectedTypes.size < DAMAGE_TYPES.size) {
|
||||||
val typeList = selectedTypes.joinToString("', '", "'", "'")
|
val typeList = selectedTypes.joinToString("', '", "'", "'")
|
||||||
whereClauses.add("Typ IN ($typeList)")
|
whereClauses.add("Typ IN ($typeList)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== STATUS-FILTER =====
|
// Filter Status
|
||||||
// Nur hinzufügen wenn nicht ALLE Status ausgewählt sind
|
|
||||||
if (selectedStatus.isNotEmpty() && selectedStatus.size < statusTypes.size) {
|
if (selectedStatus.isNotEmpty() && selectedStatus.size < statusTypes.size) {
|
||||||
val statusList = selectedStatus.joinToString("', '", "'", "'")
|
val statusList = selectedStatus.joinToString("', '", "'", "'")
|
||||||
whereClauses.add("status IN ($statusList)")
|
whereClauses.add("status IN ($statusList)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== DATUMS-FILTER =====
|
// Filter nach Datum
|
||||||
// Feldname: EditDate
|
|
||||||
if (startDate != null || endDate != null) {
|
if (startDate != null || endDate != null) {
|
||||||
val dateField = "EditDate"
|
val dateField = "EditDate"
|
||||||
|
|
||||||
@@ -411,7 +411,7 @@ suspend fun MapViewModel.applyDamageFilter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== ALLE KLAUSELN KOMBINIEREN =====
|
// Alle Klauseln kombinieren
|
||||||
val whereClause = whereClauses.joinToString(" AND ")
|
val whereClause = whereClauses.joinToString(" AND ")
|
||||||
|
|
||||||
featureLayer.definitionExpression = whereClause
|
featureLayer.definitionExpression = whereClause
|
||||||
@@ -461,7 +461,7 @@ suspend fun MapViewModel.applyDamageFilter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension-Funktion für MapViewModel
|
* Erweiterte-Funktion für MapViewModel
|
||||||
* Gibt die aktuell aktiven Filter zurück
|
* Gibt die aktuell aktiven Filter zurück
|
||||||
*/
|
*/
|
||||||
fun MapViewModel.getActiveFilters(): Set<String> {
|
fun MapViewModel.getActiveFilters(): Set<String> {
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ fun DamageListDialog(
|
|||||||
|
|
||||||
Divider(modifier = Modifier.padding(vertical = 12.dp))
|
Divider(modifier = Modifier.padding(vertical = 12.dp))
|
||||||
|
|
||||||
// ===== ENTFERNUNGS-FILTER =====
|
// Filtern nach Entfernung
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -192,7 +192,7 @@ fun DamageListDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// ===== SORTIERUNG =====
|
// Sortieren
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
text = "Sortieren nach:",
|
text = "Sortieren nach:",
|
||||||
@@ -205,7 +205,7 @@ fun DamageListDialog(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
// Distance Button
|
// Entfernung Button
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = sortBy == SortBy.DISTANCE,
|
selected = sortBy == SortBy.DISTANCE,
|
||||||
onClick = { sortBy = SortBy.DISTANCE },
|
onClick = { sortBy = SortBy.DISTANCE },
|
||||||
@@ -220,7 +220,7 @@ fun DamageListDialog(
|
|||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Relevance Button
|
// Relevanz Button
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = sortBy == SortBy.RELEVANCE,
|
selected = sortBy == SortBy.RELEVANCE,
|
||||||
onClick = { sortBy = SortBy.RELEVANCE },
|
onClick = { sortBy = SortBy.RELEVANCE },
|
||||||
@@ -239,7 +239,7 @@ fun DamageListDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// ===== RELEVANZ-FILTER (nur wenn nach Relevanz sortiert) =====
|
// Nach Relevanz filtern
|
||||||
if (sortBy == SortBy.RELEVANCE) {
|
if (sortBy == SortBy.RELEVANCE) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
@@ -380,7 +380,7 @@ fun DamageListDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Einzelnes Schadens-Item in der Liste MIT FOTO und BEWERTUNG
|
* Einzelnes Schadens-Item in der Liste mit Foto und Bewertung
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun DamageListItemWithPhoto(
|
fun DamageListItemWithPhoto(
|
||||||
@@ -401,7 +401,7 @@ fun DamageListItemWithPhoto(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// LINKS: Foto
|
//LINKS: Foto
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(100.dp)
|
.width(100.dp)
|
||||||
@@ -500,7 +500,7 @@ fun DamageListItemWithPhoto(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension-Funktion für MapViewModel
|
* Erweiterte-Funktion für MapViewModel
|
||||||
* Lädt alle Schäden im Umkreis
|
* Lädt alle Schäden im Umkreis
|
||||||
*/
|
*/
|
||||||
suspend fun MapViewModel.loadDamagesNearby(
|
suspend fun MapViewModel.loadDamagesNearby(
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import com.arcgismaps.data.ArcGISFeature
|
import com.arcgismaps.data.ArcGISFeature
|
||||||
import com.arcgismaps.data.Attachment
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -35,12 +34,13 @@ fun FeatureInfoDialog(
|
|||||||
) {
|
) {
|
||||||
if (feature == null) return
|
if (feature == null) return
|
||||||
|
|
||||||
var attachments by remember { mutableStateOf<List<Attachment>>(emptyList()) }
|
|
||||||
var photoBitmaps by remember { mutableStateOf<List<androidx.compose.ui.graphics.ImageBitmap>>(emptyList()) }
|
var photoBitmaps by remember { mutableStateOf<List<androidx.compose.ui.graphics.ImageBitmap>>(emptyList()) }
|
||||||
var isLoadingPhotos by remember { mutableStateOf(true) }
|
var isLoadingPhotos by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
// Lade Fotos beim Öffnen
|
// Lade Fotos beim Öffnen
|
||||||
LaunchedEffect(feature) {
|
LaunchedEffect(feature) {
|
||||||
|
isLoadingPhotos = true
|
||||||
try {
|
try {
|
||||||
// Lade Feature falls nötig
|
// Lade Feature falls nötig
|
||||||
if (feature.loadStatus.value != com.arcgismaps.LoadStatus.Loaded) {
|
if (feature.loadStatus.value != com.arcgismaps.LoadStatus.Loaded) {
|
||||||
@@ -49,23 +49,22 @@ fun FeatureInfoDialog(
|
|||||||
|
|
||||||
// Hole Attachments
|
// Hole Attachments
|
||||||
feature.fetchAttachments().onSuccess { fetchedAttachments ->
|
feature.fetchAttachments().onSuccess { fetchedAttachments ->
|
||||||
attachments = fetchedAttachments
|
val loadedBitmaps = mutableListOf<androidx.compose.ui.graphics.ImageBitmap>()
|
||||||
|
|
||||||
// Lade Foto-Daten
|
|
||||||
val bitmaps = mutableListOf<androidx.compose.ui.graphics.ImageBitmap>()
|
|
||||||
fetchedAttachments.forEach { attachment ->
|
fetchedAttachments.forEach { attachment ->
|
||||||
attachment.fetchData().onSuccess { data ->
|
attachment.fetchData().onSuccess { data ->
|
||||||
try {
|
try {
|
||||||
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
|
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
bitmaps.add(bitmap.asImageBitmap())
|
loadedBitmaps.add(bitmap.asImageBitmap())
|
||||||
|
// Update die UI-Liste direkt, wenn ein Bild fertig ist
|
||||||
|
photoBitmaps = loadedBitmaps.toList()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("DEBUG: Fehler beim Laden des Bildes: ${e.message}")
|
println("DEBUG: Fehler beim Decodieren des Bildes: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
photoBitmaps = bitmaps
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("DEBUG: Fehler beim Laden der Attachments: ${e.message}")
|
println("DEBUG: Fehler beim Laden der Attachments: ${e.message}")
|
||||||
@@ -95,7 +94,7 @@ fun FeatureInfoDialog(
|
|||||||
modifier = Modifier.padding(bottom = 16.dp)
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Divider(modifier = Modifier.padding(bottom = 16.dp))
|
HorizontalDivider(modifier = Modifier.padding(bottom = 16.dp))
|
||||||
|
|
||||||
// Beschreibung
|
// Beschreibung
|
||||||
val description = feature.attributes["Beschreibung"]?.toString()
|
val description = feature.attributes["Beschreibung"]?.toString()
|
||||||
@@ -113,7 +112,7 @@ fun FeatureInfoDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fotos
|
// Fotos
|
||||||
if (isLoadingPhotos) {
|
if (isLoadingPhotos && photoBitmaps.isEmpty()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -122,7 +121,7 @@ fun FeatureInfoDialog(
|
|||||||
) {
|
) {
|
||||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text("Lade Fotos...")
|
Text("Suche Fotos...")
|
||||||
}
|
}
|
||||||
} else if (photoBitmaps.isNotEmpty()) {
|
} else if (photoBitmaps.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
@@ -149,7 +148,6 @@ fun FeatureInfoDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +198,6 @@ fun FeatureInfoDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info-Text
|
|
||||||
Text(
|
Text(
|
||||||
text = "Hast du diesen Schaden auch gesehen?",
|
text = "Hast du diesen Schaden auch gesehen?",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
@@ -211,24 +208,19 @@ fun FeatureInfoDialog(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {//Daumen runter
|
||||||
// Daumen runter
|
Button(
|
||||||
OutlinedButton(
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onRate(feature, false)
|
onRate(feature, false)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier.padding(vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Text("👎", style = MaterialTheme.typography.headlineMedium)
|
Text("👎", style = MaterialTheme.typography.headlineMedium)
|
||||||
Text("Nein", style = MaterialTheme.typography.bodyMedium)
|
Text("Nein", style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Daumen hoch
|
// Daumen hoch
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -237,17 +229,13 @@ fun FeatureInfoDialog(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier.padding(vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Text("👍", style = MaterialTheme.typography.headlineMedium)
|
Text("👍", style = MaterialTheme.typography.headlineMedium)
|
||||||
Text("Ja", style = MaterialTheme.typography.bodyMedium)
|
Text("Ja", style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schließen Button
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onDismiss,
|
onClick = onDismiss,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -261,10 +249,8 @@ fun FeatureInfoDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension-Funktion für MapViewModel
|
* Erweiterungs-Funktion für MapViewModel
|
||||||
* Aktualisiert die Community-Bewertung eines Features
|
* Aktualisiert die Community-Bewertung eines Features
|
||||||
*/
|
*/
|
||||||
suspend fun MapViewModel.updateFeatureRating(
|
suspend fun MapViewModel.updateFeatureRating(
|
||||||
@@ -274,62 +260,31 @@ suspend fun MapViewModel.updateFeatureRating(
|
|||||||
): Boolean {
|
): Boolean {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// Hole aktuelle Bewertung
|
|
||||||
val currentRating = (feature.attributes["communitycounter"] as? Number)?.toInt() ?: 0
|
val currentRating = (feature.attributes["communitycounter"] as? Number)?.toInt() ?: 0
|
||||||
|
val newRating = if (isPositive) currentRating + 1 else maxOf(0, currentRating - 1)
|
||||||
|
|
||||||
// Berechne neue Bewertung
|
|
||||||
val newRating = if (isPositive) {
|
|
||||||
currentRating + 1
|
|
||||||
} else {
|
|
||||||
maxOf(0, currentRating - 1) // Verhindert negative Werte
|
|
||||||
}
|
|
||||||
|
|
||||||
println("DEBUG: Rating-Update: $currentRating -> $newRating (${if(isPositive) "+" else "-"})")
|
|
||||||
|
|
||||||
// Aktualisiere Feature-Attribut
|
|
||||||
feature.attributes["communitycounter"] = newRating
|
feature.attributes["communitycounter"] = newRating
|
||||||
|
|
||||||
// Speichere in Feature Table
|
|
||||||
serviceFeatureTable.updateFeature(feature).onSuccess {
|
serviceFeatureTable.updateFeature(feature).onSuccess {
|
||||||
println("DEBUG: updateFeature erfolgreich")
|
|
||||||
|
|
||||||
// Synchronisiere mit ArcGIS Online
|
|
||||||
serviceFeatureTable.applyEdits().onSuccess {
|
serviceFeatureTable.applyEdits().onSuccess {
|
||||||
println("DEBUG: applyEdits erfolgreich - Rating gespeichert")
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val message = if (isPositive) {
|
val message = if (isPositive) "✓ Schaden bestätigt!" else "✓ Bewertung verringert"
|
||||||
"✓ Schaden bestätigt! (${currentRating} → ${newRating})"
|
|
||||||
} else {
|
|
||||||
"✓ Bewertung verringert (${currentRating} → ${newRating})"
|
|
||||||
}
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||||
snackBarMessage = message
|
|
||||||
}
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
println("DEBUG: applyEdits fehlgeschlagen: ${error.message}")
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
Toast.makeText(context, "Sync-Fehler: ${error.message}", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, "Sync-Fehler: ${error.message}", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
println("DEBUG: updateFeature fehlgeschlagen: ${error.message}")
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
Toast.makeText(context, "Update-Fehler: ${error.message}", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, "Update-Fehler: ${error.message}", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("DEBUG: Rating-Update Exception: ${e.message}")
|
|
||||||
e.printStackTrace()
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
Toast.makeText(
|
Toast.makeText(context, "Fehler: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
context,
|
|
||||||
"Fehler beim Bewerten: ${e.message}",
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ fun SettingsScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isProximityActive by ProximityNotificationService.isRunning.collectAsState()
|
val isProximityActive by ProximityNotificationService.isRunning.collectAsState()
|
||||||
|
|
||||||
// NEU: Notification Permission Launcher
|
// Notification Permission Launcher
|
||||||
val notificationPermissionLauncher = rememberLauncherForActivityResult(
|
val notificationPermissionLauncher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission()
|
ActivityResultContracts.RequestPermission()
|
||||||
) { isGranted ->
|
) { isGranted ->
|
||||||
@@ -65,7 +65,7 @@ fun SettingsScreen(
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
// Benachrichtigungen Section
|
// Benachrichtigungen
|
||||||
Text(
|
Text(
|
||||||
text = "Benachrichtigungen",
|
text = "Benachrichtigungen",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
@@ -180,7 +180,7 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Weitere Einstellungen können hier hinzugefügt werden
|
|
||||||
Text(
|
Text(
|
||||||
text = "Informationen",
|
text = "Informationen",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ class LocationHelper(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Composable zum Einrichten des Location Display
|
* Composable zum Einrichten des Location Display
|
||||||
*
|
*
|
||||||
* @param autoPanMode Wie die Karte dem Standort folgen soll (default: Recenter)
|
|
||||||
* @return LocationDisplay-Objekt, das an MapView übergeben werden kann
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun setupLocationDisplay(
|
fun setupLocationDisplay(
|
||||||
|
|||||||
@@ -1,40 +1,77 @@
|
|||||||
# Album/Kamera-System Dokumentation
|
# AlbumViewModel
|
||||||
|
|
||||||
## AlbumViewModel
|
## Übersicht
|
||||||
|
Die Klasse `AlbumViewModel` ist für die zentrale Verwaltung von Bilddaten innerhalb der App zuständig. Sie fungiert als Schnittstelle zwischen der Kamera-Hardware, der System-Galerie und der Benutzeroberfläche. Das ViewModel verarbeitet asynchrone Bildoperationen und stellt den aktuellen Status über einen reaktiven StateFlow bereit.
|
||||||
```kotlin
|
|
||||||
class AlbumViewModel(private val coroutineContext: CoroutineContext) : ViewModel()
|
|
||||||
```
|
|
||||||
|
|
||||||
**Zweck:** Verwaltung von Bildauswahl und Kamera-Aufnahmen für Schadensmeldungen.
|
|
||||||
|
|
||||||
### Properties
|
|
||||||
|
|
||||||
| Name | Typ | Beschreibung |
|
|
||||||
|------|-----|--------------|
|
|
||||||
| `viewStateFlow` | `StateFlow<AlbumViewState>` | Read-only State für UI-Komponenten |
|
|
||||||
|
|
||||||
### Methoden
|
|
||||||
|
|
||||||
#### `onReceive(intent: Intent)`
|
|
||||||
Verarbeitet Benutzeraktionen für Bild-Verwaltung.
|
|
||||||
|
|
||||||
**Parameter:**
|
|
||||||
- `intent: Intent` - Benutzeraktion (siehe Intent-Klasse)
|
|
||||||
|
|
||||||
**Verwendete Intents:**
|
|
||||||
- `OnPermissionGrantedWith(Context)` - Erstellt temp. Datei für Kamera
|
|
||||||
- `OnFinishPickingImagesWith(Context, List<Uri>)` - Lädt Bilder aus Galerie
|
|
||||||
- `OnImageSavedWith(Context)` - Speichert Kamera-Aufnahme
|
|
||||||
- `OnImageSavingCanceled` - Verwirft temp. Datei
|
|
||||||
- `OnPermissionDenied` - Loggt Permission-Verweigerung
|
|
||||||
|
|
||||||
**Deprecated Intents:** `OnPermissionGranted`, `OnFinishPickingImages`, `OnImageSaved` (ohne Context)
|
|
||||||
|
|
||||||
#### `clearSelection()`
|
|
||||||
Löscht alle ausgewählten Bilder aus dem State.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 1. Klasse: AlbumViewModel
|
||||||
|
`class AlbumViewModel(private val coroutineContext: CoroutineContext) : ViewModel()`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Konstruktor-Parameter
|
||||||
|
| Parameter | Typ | Beschreibung |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `coroutineContext` | `CoroutineContext` | Der Kontext, in dem die Coroutines für Bildoperationen ausgeführt werden. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Status-Management (State)
|
||||||
|
|
||||||
|
### `viewStateFlow: StateFlow<AlbumViewState>`
|
||||||
|
Ein observierbarer Datenstrom, der den aktuellen Zustand der Bildverwaltung liefert. Er basiert auf der Datenklasse `AlbumViewState`.
|
||||||
|
|
||||||
|
**Wichtige Felder im State:**
|
||||||
|
* **`tempFileUrl`**: Enthält die URL der temporären Datei, in der das mit der Kamera aufgenommene Bild zwischengespeichert wird.
|
||||||
|
* **`selectedPictures`**: Enthält die Liste der mit der Kamera aufgenommenen oder aus der Galerie ausgewählten Bilder (als `ImageBitmap`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Zentrale Methoden
|
||||||
|
|
||||||
|
### `onReceive(intent: Intent)`
|
||||||
|
Diese Methode ist der zentrale Einstiegspunkt für alle Aktionen. Sie verarbeitet verschiedene `Intent`-Typen:
|
||||||
|
|
||||||
|
| Intent | Beschreibung |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `OnPermissionGrantedWith` | Wird aufgerufen, wenn die Kamera-Berechtigung erteilt wurde. Erstellt eine temporäre Datei (`.jpg`) im Cache-Verzeichnis und generiert eine Inhalts-URI via `FileProvider`. |
|
||||||
|
| `OnPermissionDenied` | Loggt die Verweigerung der Kamera-Berechtigung durch den Nutzer. |
|
||||||
|
| `OnFinishPickingImagesWith` | Verarbeitet Bilder, die aus der Galerie ausgewählt wurden. Die URIs werden in `ImageBitmap` konvertiert und der Liste hinzugefügt. |
|
||||||
|
| `OnImageSavedWith` | Wird nach einer erfolgreichen Kameraaufnahme aufgerufen. Dekodiert das Bild aus der `tempFileUrl` und fügt es der Auswahl hinzu. |
|
||||||
|
| `OnImageSavingCanceled` | Setzt die `tempFileUrl` zurück, falls der Aufnahmevorgang abgebrochen wurde. |
|
||||||
|
|
||||||
|
### `clearSelection()`
|
||||||
|
Setzt die Liste der ausgewählten Bilder (`selectedPictures`) auf eine leere Liste zurück.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Funktionsweise & Datenfluss
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Bildverarbeitung (Galerie)
|
||||||
|
Beim Auswählen von Bildern aus der Galerie werden die `InputStreams` der bereitgestellten URIs ausgelesen. Um Speicher effizient zu nutzen, werden die Byte-Arrays mithilfe von `BitmapFactory` dekodiert und anschließend als Compose-kompatible `ImageBitmap` gespeichert.
|
||||||
|
|
||||||
|
### Bildverarbeitung (Kamera)
|
||||||
|
1. **Vorbereitung**: Ein `File.createTempFile` erstellt einen Platzhalter im Cache.
|
||||||
|
2. **Sicherheit**: Der `FileProvider` wandelt den Dateipfad in eine sichere URI um, damit die Kamera-App darauf zugreifen kann.
|
||||||
|
3. **Abschluss**: Nach der Aufnahme wird `ImageDecoder` genutzt, um die Datei in eine Bitmap umzuwandeln.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Technische Voraussetzungen
|
||||||
|
|
||||||
|
### FileProvider-Konfiguration
|
||||||
|
Damit die Kamera-App Bilder speichern kann, muss in der `AndroidManifest.xml` ein Provider definiert sein, der auf `${BuildConfig.APPLICATION_ID}.provider` hört.
|
||||||
|
|
||||||
|
### Abhängigkeiten
|
||||||
|
* **androidx.lifecycle:lifecycle-viewmodel-ktx**: Für den `viewModelScope`.
|
||||||
|
* **kotlinx-coroutines**: Für die asynchrone Verarbeitung der Bilddaten.
|
||||||
|
* **androidx.compose.ui:ui-graphics**: Für die Konvertierung in `ImageBitmap`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
* **NULL-Werte**: Falls ein InputStream nicht gelesen werden kann, wird eine Fehlermeldung geloggt, ohne die App zum Absturz zu bringen.
|
||||||
|
* **Speichermanagement**: Bilder werden als Bitmaps im Speicher gehalten. Bei sehr großen Mengen sollte eine Skalierung (Sampling) in der `BitmapFactory` implementiert werden.
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
# DamageFilterSystem
|
||||||
|
|
||||||
|
**Zweck:** Filter-System für Feature Layer. Ermöglicht Filterung nach Schadenstyp, Bearbeitungsstatus und Datum (unabhängig kombinierbar).
|
||||||
|
|
||||||
|
**Komponenten:** UI-Dialog + Extension-Funktionen für MapViewModel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FilterCheckboxItem
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Composable
|
||||||
|
fun FilterCheckboxItem(
|
||||||
|
label: String,
|
||||||
|
isChecked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
emoji: String = "•"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Wiederverwendbare Checkbox-Komponente mit Label und Emoji.
|
||||||
|
|
||||||
|
**Parameter:**
|
||||||
|
|
||||||
|
| Name | Typ | Beschreibung |
|
||||||
|
|------|-----|--------------|
|
||||||
|
| `label` | String | Angezeigter Text |
|
||||||
|
| `isChecked` | Boolean | Checkbox-Status |
|
||||||
|
| `onCheckedChange` | (Boolean) -> Unit | Callback bei Status-Änderung |
|
||||||
|
| `emoji` | String | Icon/Emoji rechts (default: "•") |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DamageFilterDialog
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Composable
|
||||||
|
fun DamageFilterDialog(
|
||||||
|
damageTypes: List<String>,
|
||||||
|
currentFilters: Set<String>,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onApplyFilter: (Set<String>, Set<String>, LocalDate?, LocalDate?) -> Unit
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Vollbildschirm-Dialog zur Auswahl von Filter-Kriterien.
|
||||||
|
|
||||||
|
**Parameter:**
|
||||||
|
|
||||||
|
| Name | Typ | Beschreibung |
|
||||||
|
|------|-----|--------------|
|
||||||
|
| `damageTypes` | List<String> | Verfügbare Schadenstypen (z.B. "Straße", "Gehweg") |
|
||||||
|
| `currentFilters` | Set<String> | Aktuell aktive Typ-Filter |
|
||||||
|
| `onDismiss` | () -> Unit | Callback zum Schließen des Dialogs |
|
||||||
|
| `onApplyFilter` | (Set<String>, Set<String>, LocalDate?, LocalDate?) -> Unit | Callback mit (Typen, Status, StartDatum, EndDatum) |
|
||||||
|
|
||||||
|
### Filter-Typen
|
||||||
|
|
||||||
|
**1. Schadenstypen:**
|
||||||
|
- Straße 🛣️
|
||||||
|
- Gehweg 🚶
|
||||||
|
- Fahrradweg 🚴
|
||||||
|
- Beleuchtung 💡
|
||||||
|
- Sonstiges 📍
|
||||||
|
|
||||||
|
**2. Bearbeitungsstatus:**
|
||||||
|
- neu 🔴
|
||||||
|
- in Bearbeitung 🟠
|
||||||
|
- Schaden behoben 🟢
|
||||||
|
|
||||||
|
**3. Datums-Filter:**
|
||||||
|
- Von-Datum (dd.MM.yyyy)
|
||||||
|
- Bis-Datum (dd.MM.yyyy)
|
||||||
|
- Optional aktivierbar via Checkbox
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
|
||||||
|
| State | Typ | Default | Beschreibung |
|
||||||
|
|-------|-----|---------|--------------|
|
||||||
|
| `selectedFilters` | Set<String> | alle Typen | Ausgewählte Schadenstypen |
|
||||||
|
| `selectedStatus` | Set<String> | alle Status | Ausgewählte Status |
|
||||||
|
| `startDateString` | String | "" | Eingabe Von-Datum |
|
||||||
|
| `endDateString` | String | "" | Eingabe Bis-Datum |
|
||||||
|
| `startDate` | LocalDate? | null | Geparste Von-Datum |
|
||||||
|
| `endDate` | LocalDate? | null | Geparste Bis-Datum |
|
||||||
|
| `useDateFilter` | Boolean | false | Datums-Filter aktiv |
|
||||||
|
|
||||||
|
### UI-Buttons
|
||||||
|
|
||||||
|
| Button | Funktion |
|
||||||
|
|--------|----------|
|
||||||
|
| "Alle" | Wählt alle Typen + alle Status |
|
||||||
|
| "Keine" | Deselektiert alle Typen + alle Status |
|
||||||
|
| "Filter anwenden" | Ruft `onApplyFilter()` auf und schließt Dialog |
|
||||||
|
|
||||||
|
### Datums-Parsing
|
||||||
|
|
||||||
|
**Format:** dd.MM.yyyy (z.B. 15.01.2024)
|
||||||
|
|
||||||
|
**Validierung:**
|
||||||
|
```kotlin
|
||||||
|
LocalDate.parse(input, DateTimeFormatter.ofPattern("dd.MM.yyyy"))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parsing:** Automatisch bei Eingabe (length == 10), fehlerhafte Eingaben werden ignoriert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## applyDamageFilter (Extension Function)
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
suspend fun MapViewModel.applyDamageFilter(
|
||||||
|
selectedTypes: Set<String>,
|
||||||
|
selectedStatus: Set<String>,
|
||||||
|
startDate: LocalDate? = null,
|
||||||
|
endDate: LocalDate? = null
|
||||||
|
): Boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Wendet Filter auf FeatureLayer via SQL WHERE-Clause an.
|
||||||
|
|
||||||
|
**Parameter:**
|
||||||
|
|
||||||
|
| Name | Typ | Beschreibung |
|
||||||
|
|------|-----|--------------|
|
||||||
|
| `selectedTypes` | Set<String> | Ausgewählte Schadenstypen |
|
||||||
|
| `selectedStatus` | Set<String> | Ausgewählte Status |
|
||||||
|
| `startDate` | LocalDate? | Start-Datum (optional) |
|
||||||
|
| `endDate` | LocalDate? | End-Datum (optional) |
|
||||||
|
|
||||||
|
**Return:** `Boolean` - true bei Erfolg, false bei Fehler
|
||||||
|
|
||||||
|
### Filter-Logik
|
||||||
|
|
||||||
|
**1. Typ-Filter:**
|
||||||
|
```sql
|
||||||
|
Typ IN ('Straße', 'Gehweg')
|
||||||
|
```
|
||||||
|
- Nur hinzugefügt wenn nicht alle Typen ausgewählt
|
||||||
|
- Verwendet SQL IN-Operator
|
||||||
|
|
||||||
|
**2. Status-Filter:**
|
||||||
|
```sql
|
||||||
|
status IN ('neu', 'in Bearbeitung')
|
||||||
|
```
|
||||||
|
- Nur hinzugefügt wenn nicht alle Status ausgewählt
|
||||||
|
- Verwendet SQL IN-Operator
|
||||||
|
|
||||||
|
**3. Datums-Filter:**
|
||||||
|
```sql
|
||||||
|
EditDate >= timestamp '2024-01-01 00:00:00'
|
||||||
|
AND EditDate <= timestamp '2024-12-31 23:59:59'
|
||||||
|
```
|
||||||
|
- Feldname: `EditDate`
|
||||||
|
- Format: SQL timestamp
|
||||||
|
- Unterstützt: Von, Bis, oder beides
|
||||||
|
|
||||||
|
**Kombination:**
|
||||||
|
```sql
|
||||||
|
Typ IN ('Straße') AND status IN ('neu') AND EditDate >= timestamp '...'
|
||||||
|
```
|
||||||
|
- Alle aktiven Filter werden mit AND verknüpft
|
||||||
|
|
||||||
|
### Snackbar-Feedback
|
||||||
|
|
||||||
|
**Format:** "Filter: {Typ-Info} | {Status-Info} | {Datum-Info}"
|
||||||
|
|
||||||
|
**Beispiele:**
|
||||||
|
- "Filter: 3 Typ(en) | 2 Status"
|
||||||
|
- "Filter: Datum: 01.01.2024 - 31.12.2024"
|
||||||
|
- "Alle Schäden werden angezeigt" (keine Filter)
|
||||||
|
|
||||||
|
### Threading
|
||||||
|
|
||||||
|
**Ausführung:** `withContext(Dispatchers.IO)` für Feature Query
|
||||||
|
|
||||||
|
**UI-Updates:** `withContext(Dispatchers.Main)` für snackBarMessage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## getActiveFilters (Extension Function)
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun MapViewModel.getActiveFilters(): Set<String>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Extrahiert aktuell aktive Filter aus FeatureLayer.definitionExpression.
|
||||||
|
|
||||||
|
**Return:** Set<String> - Menge der gefilterten Werte (z.B. {"Straße", "Gehweg"})
|
||||||
|
|
||||||
|
**Extraktion:**
|
||||||
|
```kotlin
|
||||||
|
val regex = "'([^']+)'".toRegex()
|
||||||
|
regex.findAll(expression).map { it.groupValues[1] }.toSet()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```
|
||||||
|
Expression: "Typ IN ('Straße', 'Gehweg')"
|
||||||
|
Return: {"Straße", "Gehweg"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwendungsbeispiel
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Dialog anzeigen
|
||||||
|
var showFilterDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showFilterDialog) {
|
||||||
|
DamageFilterDialog(
|
||||||
|
damageTypes = MapViewModel.DAMAGE_TYPES,
|
||||||
|
currentFilters = mapViewModel.getActiveFilters(),
|
||||||
|
onDismiss = { showFilterDialog = false },
|
||||||
|
onApplyFilter = { types, status, start, end ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
mapViewModel.applyDamageFilter(types, status, start, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aus MainScreen
|
||||||
|
SliderMenuItem(
|
||||||
|
text = "Schäden filtern",
|
||||||
|
icon = Icons.Default.FilterAlt,
|
||||||
|
onClick = { showFilterDialog = true }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SQL-Beispiele
|
||||||
|
|
||||||
|
**Nur Typ:**
|
||||||
|
```sql
|
||||||
|
Typ IN ('Straße', 'Gehweg')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nur Status:**
|
||||||
|
```sql
|
||||||
|
status IN ('neu', 'in Bearbeitung')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nur Datum:**
|
||||||
|
```sql
|
||||||
|
EditDate >= timestamp '2024-01-01 00:00:00'
|
||||||
|
AND EditDate <= timestamp '2024-12-31 23:59:59'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alle kombiniert:**
|
||||||
|
```sql
|
||||||
|
Typ IN ('Straße')
|
||||||
|
AND status IN ('neu')
|
||||||
|
AND EditDate >= timestamp '2024-01-01 00:00:00'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Keine Filter (alle anzeigen):**
|
||||||
|
```sql
|
||||||
|
(empty string)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature-Attribute
|
||||||
|
|
||||||
|
**Erforderliche Felder im Feature Layer:**
|
||||||
|
|
||||||
|
| Feldname | Typ | Werte | Beschreibung |
|
||||||
|
|----------|-----|-------|--------------|
|
||||||
|
| `Typ` | String | "Straße", "Gehweg", etc. | Schadenstyp |
|
||||||
|
| `status` | String | "neu", "in Bearbeitung", "Schaden behoben" | Bearbeitungsstatus |
|
||||||
|
| `EditDate` | Timestamp | SQL timestamp | Letzte Änderung |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technische Details
|
||||||
|
|
||||||
|
- **UI Framework:** Jetpack Compose
|
||||||
|
- **Dialog:** Material3 Card im Dialog
|
||||||
|
- **State:** remember/mutableStateOf
|
||||||
|
- **Threading:** Coroutines (Dispatchers.IO/Main)
|
||||||
|
- **SQL:** ArcGIS SQL-Syntax (definitionExpression)
|
||||||
|
- **Datum:** Java Time API (LocalDate, DateTimeFormatter)
|
||||||
|
|||||||
@@ -0,0 +1,387 @@
|
|||||||
|
# DamageListDialog
|
||||||
|
|
||||||
|
**Zweck:** Dialog zur Anzeige von Straßenschäden in der Nähe mit Foto-Vorschau, Entfernungs- und Relevanz-Filter.
|
||||||
|
|
||||||
|
**Features:** GPS-basierte Entfernungsberechnung, Sortierung nach Distanz/Relevanz, dynamischer Radius-Filter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Classes
|
||||||
|
|
||||||
|
### DamageWithDistance
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
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
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Container für Feature mit berechneter Distanz und geladenen Daten.
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
| Name | Typ | Beschreibung |
|
||||||
|
|------|-----|--------------|
|
||||||
|
| `feature` | ArcGISFeature | Originales ArcGIS Feature |
|
||||||
|
| `distanceInMeters` | Double? | Entfernung zum Nutzer in Metern |
|
||||||
|
| `typ` | String | Schadenstyp (z.B. "Straße") |
|
||||||
|
| `beschreibung` | String | Schadensbeschreibung |
|
||||||
|
| `objectId` | Long | Feature OBJECTID |
|
||||||
|
| `rating` | Int | Community-Bewertungen (communitycounter) |
|
||||||
|
| `photo` | ImageBitmap? | Erstes Attachment als Bitmap (optional) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enums
|
||||||
|
|
||||||
|
### SortBy
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
enum class SortBy {
|
||||||
|
DISTANCE, // Nach Entfernung sortieren (nächste zuerst)
|
||||||
|
RELEVANCE // Nach Relevanz sortieren (höchste communitycounter zuerst)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Sortierungs-Modi für Schadensliste.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
### calculateDistance
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun calculateDistance(
|
||||||
|
lat1: Double,
|
||||||
|
lon1: Double,
|
||||||
|
lat2: Double,
|
||||||
|
lon2: Double
|
||||||
|
): Double
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Berechnet Luftlinie zwischen zwei GPS-Koordinaten.
|
||||||
|
|
||||||
|
**Parameter:**
|
||||||
|
|
||||||
|
| Name | Typ | Beschreibung |
|
||||||
|
|------|-----|--------------|
|
||||||
|
| `lat1` | Double | Breitengrad Punkt 1 |
|
||||||
|
| `lon1` | Double | Längengrad Punkt 1 |
|
||||||
|
| `lat2` | Double | Breitengrad Punkt 2 |
|
||||||
|
| `lon2` | Double | Längengrad Punkt 2 |
|
||||||
|
|
||||||
|
**Return:** Entfernung in Metern
|
||||||
|
|
||||||
|
**Algorithmus:** Haversine-Formel
|
||||||
|
```kotlin
|
||||||
|
R = 6371000.0 // Erdradius in Metern
|
||||||
|
dLat = toRadians(lat2 - lat1)
|
||||||
|
dLon = toRadians(lon2 - lon1)
|
||||||
|
a = sin²(dLat/2) + cos(lat1) * cos(lat2) * sin²(dLon/2)
|
||||||
|
c = 2 * atan2(√a, √(1-a))
|
||||||
|
distance = R * c
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Composables
|
||||||
|
|
||||||
|
### DamageListDialog
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Composable
|
||||||
|
fun DamageListDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onDamageClick: (ArcGISFeature) -> Unit,
|
||||||
|
mapViewModel: MapViewModel
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Haupt-Dialog mit Filter- und Sortieroptionen.
|
||||||
|
|
||||||
|
**Parameter:**
|
||||||
|
|
||||||
|
| Name | Typ | Beschreibung |
|
||||||
|
|------|-----|--------------|
|
||||||
|
| `onDismiss` | () -> Unit | Callback zum Schließen |
|
||||||
|
| `onDamageClick` | (ArcGISFeature) -> Unit | Callback bei Feature-Auswahl |
|
||||||
|
| `mapViewModel` | MapViewModel | ViewModel für Feature-Zugriff |
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
|
||||||
|
| State | Typ | Default | Beschreibung |
|
||||||
|
|-------|-----|---------|--------------|
|
||||||
|
| `damages` | List<DamageWithDistance> | emptyList() | Geladene Schäden |
|
||||||
|
| `isLoading` | Boolean | true |Lade-Status |
|
||||||
|
| `maxDistance` | Float | 1000f | Radius-Filter in Metern |
|
||||||
|
| `userLocation` | Point? | null | GPS-Position |
|
||||||
|
| `errorMessage` | String? | null | Fehlermeldung |
|
||||||
|
| `sortBy` | SortBy | DISTANCE | Sortierungs-Modus |
|
||||||
|
| `minRelevance` | Int | 0 | Minimale Bewertungen |
|
||||||
|
|
||||||
|
### Filter-Optionen
|
||||||
|
|
||||||
|
**1. Entfernungs-Filter:**
|
||||||
|
- **Range:** 100m - 5000m (5km)
|
||||||
|
- **Steps:** 48
|
||||||
|
- **UI:** Slider mit Icon 📍
|
||||||
|
- **Anzeige:** "{Distanz} m" oder "{Distanz} km"
|
||||||
|
|
||||||
|
**2. Sortierung:**
|
||||||
|
- **DISTANCE:** Nach Entfernung (nächste zuerst)
|
||||||
|
- **RELEVANCE:** Nach communitycounter (höchste zuerst)
|
||||||
|
- **UI:** FilterChips "📍 Entfernung" / "👥 Relevanz"
|
||||||
|
|
||||||
|
**3. Relevanz-Filter (nur bei RELEVANCE):**
|
||||||
|
- **Range:** 0 - 50+
|
||||||
|
- **Steps:** 49
|
||||||
|
- **UI:** Slider mit Icon 📈
|
||||||
|
- **Anzeige:** "Min. Bewertungen: {count}+"
|
||||||
|
|
||||||
|
### UI-Zustände
|
||||||
|
|
||||||
|
**Loading:**
|
||||||
|
```
|
||||||
|
CircularProgressIndicator
|
||||||
|
"Lade Schäden..."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
⚠️ Emoji
|
||||||
|
Fehlermeldung (rot)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Empty:**
|
||||||
|
```
|
||||||
|
🔍 Emoji
|
||||||
|
"Keine Schäden im Umkreis"
|
||||||
|
oder "Keine Schäden mit dieser Bewertung"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success:**
|
||||||
|
```
|
||||||
|
LazyColumn mit DamageListItemWithPhoto
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DamageListItemWithPhoto
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Composable
|
||||||
|
fun DamageListItemWithPhoto(
|
||||||
|
damage: DamageWithDistance,
|
||||||
|
onClick: () -> Unit
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Einzelne Zeile in der Schadensliste.
|
||||||
|
|
||||||
|
**Parameter:**
|
||||||
|
|
||||||
|
| Name | Typ | Beschreibung |
|
||||||
|
|------|-----|--------------|
|
||||||
|
| `damage` | DamageWithDistance | Anzuzeigender Schaden |
|
||||||
|
| `onClick` | () -> Unit | Callback bei Klick |
|
||||||
|
|
||||||
|
**Layout:**
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ ┌──────┐ {Emoji} {Typ} │
|
||||||
|
│ │ │ {Beschreibung} │
|
||||||
|
│ │ Foto │ 📍 {Distanz} 👥 {Rating} │
|
||||||
|
│ └──────┘ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Foto-Box:**
|
||||||
|
- **Größe:** 100x100 dp
|
||||||
|
- **Shape:** RoundedCornerShape (links abgerundet)
|
||||||
|
- **Fallback:** 📷 Emoji auf grauem Hintergrund
|
||||||
|
|
||||||
|
**Info-Bereich:**
|
||||||
|
- **Typ:** Emoji + Name (Bold)
|
||||||
|
- **Beschreibung:** Max. 40 Zeichen + "..."
|
||||||
|
- **Distanz:** 📍 + formatDistance()
|
||||||
|
- **Rating:** 👥 + communitycounter (farbcodiert)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Extension Functions
|
||||||
|
|
||||||
|
### MapViewModel.loadDamagesNearby
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
suspend fun MapViewModel.loadDamagesNearby(
|
||||||
|
userLocation: Point,
|
||||||
|
maxDistanceMeters: Double,
|
||||||
|
context: Context
|
||||||
|
): List<DamageWithDistance>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Lädt alle Features im Umkreis mit Distanz-Berechnung und Foto-Loading.
|
||||||
|
|
||||||
|
**Parameter:**
|
||||||
|
|
||||||
|
| Name | Typ | Beschreibung |
|
||||||
|
|------|-----|--------------|
|
||||||
|
| `userLocation` | Point | GPS-Position des Nutzers |
|
||||||
|
| `maxDistanceMeters` | Double | Maximaler Radius |
|
||||||
|
| `context` | Context | Für Toast-Nachrichten |
|
||||||
|
|
||||||
|
**Return:** List<DamageWithDistance> - Sortiert nach Entfernung
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
|
||||||
|
1. **Query:** Alle Features (`whereClause = "1=1"`)
|
||||||
|
2. **Für jedes Feature:**
|
||||||
|
- Load Feature-Daten
|
||||||
|
- Geometrie extrahieren
|
||||||
|
- Koordinaten-Transformation (GeometryEngine.projectOrNull)
|
||||||
|
- Distanz-Berechnung (Haversine)
|
||||||
|
- Filter: nur wenn ≤ maxDistanceMeters
|
||||||
|
- Attribute extrahieren (Typ, Beschreibung, OBJECTID, communitycounter)
|
||||||
|
- Erstes Attachment laden und zu Bitmap konvertieren
|
||||||
|
3. **Return:** Nach Distanz sortierte Liste
|
||||||
|
|
||||||
|
**Koordinaten-Transformation:**
|
||||||
|
```kotlin
|
||||||
|
GeometryEngine.projectOrNull(
|
||||||
|
featureGeometry,
|
||||||
|
userLocation.spatialReference
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Foto-Loading:**
|
||||||
|
```kotlin
|
||||||
|
val attachments = feature.fetchAttachments().getOrNull()
|
||||||
|
val firstAttachment = attachments?.firstOrNull()
|
||||||
|
val data = firstAttachment?.fetchData().getOrNull()
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Handling:**
|
||||||
|
- Try-Catch um gesamte Funktion
|
||||||
|
- Toast bei Fehler
|
||||||
|
- Return emptyList() bei Fehler
|
||||||
|
|
||||||
|
**Debug-Logging:**
|
||||||
|
- Query-Status
|
||||||
|
- Feature-Count
|
||||||
|
- Distanz-Berechnungen
|
||||||
|
- Transformation-Fehler
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Helper Functions
|
||||||
|
|
||||||
|
### formatDistance
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun formatDistance(meters: Double?): String
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Formatiert Distanz-Anzeige.
|
||||||
|
|
||||||
|
**Logic:**
|
||||||
|
- `< 1000m`: "{meters} m"
|
||||||
|
- `≥ 1000m`: "{km} km" (1 Dezimalstelle)
|
||||||
|
|
||||||
|
**Beispiele:**
|
||||||
|
- `250.0` → "250 m"
|
||||||
|
- `1500.0` → "1.5 km"
|
||||||
|
|
||||||
|
### getEmojiForType
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun getEmojiForType(typ: String): String
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zweck:** Mapt Schadenstyp zu Emoji.
|
||||||
|
|
||||||
|
**Mapping:**
|
||||||
|
|
||||||
|
| Typ | Emoji |
|
||||||
|
|-----|-------|
|
||||||
|
| "Straße" | 🛣️ |
|
||||||
|
| "Gehweg" | 🚶 |
|
||||||
|
| "Fahrradweg" | 🚴 |
|
||||||
|
| "Beleuchtung" | 💡 |
|
||||||
|
| "Sonstiges" | 📍 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwendungsbeispiel
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// In MainScreen
|
||||||
|
var showDamageList by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showDamageList) {
|
||||||
|
DamageListDialog(
|
||||||
|
onDismiss = { showDamageList = false },
|
||||||
|
onDamageClick = { feature ->
|
||||||
|
mapViewModel.selectedFeature = feature
|
||||||
|
mapViewModel.showFeatureInfo = true
|
||||||
|
showDamageList = false
|
||||||
|
},
|
||||||
|
mapViewModel = mapViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aus SideSlider
|
||||||
|
SliderMenuItem(
|
||||||
|
text = "Schadensliste",
|
||||||
|
icon = Icons.Default.FormatListNumbered,
|
||||||
|
onClick = { showDamageList = true }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature-Attribute
|
||||||
|
|
||||||
|
**Erforderliche Felder:**
|
||||||
|
|
||||||
|
| Feldname | Typ | Beschreibung |
|
||||||
|
|----------|-----|--------------|
|
||||||
|
| `Typ` | String | Schadenstyp |
|
||||||
|
| `Beschreibung` | String | Schadensbeschreibung |
|
||||||
|
| `OBJECTID` | Long | Feature-ID |
|
||||||
|
| `communitycounter` | Int | Community-Bewertungen |
|
||||||
|
| Attachments | Blob | Fotos (optional) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance-Hinweise
|
||||||
|
|
||||||
|
**Optimierungen:**
|
||||||
|
- Fotos werden parallel geladen (async per Feature)
|
||||||
|
- Distanz-Filter reduziert verarbeitete Features
|
||||||
|
- LazyColumn für effizientes Rendering
|
||||||
|
|
||||||
|
**Potenzielle Bottlenecks:**
|
||||||
|
- Foto-Loading bei vielen Features (async pro Feature)
|
||||||
|
- Koordinaten-Transformation (GeometryEngine)
|
||||||
|
- Haversine-Berechnung (für jedes Feature)
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technische Details
|
||||||
|
|
||||||
|
- **UI Framework:** Jetpack Compose
|
||||||
|
- **Threading:** Dispatchers.IO für Feature-Loading
|
||||||
|
- **Geometrie:** ArcGIS GeometryEngine für Transformation
|
||||||
|
- **Distanz:** Haversine-Formel
|
||||||
|
- **Fotos:** BitmapFactory + ImageBitmap
|
||||||
|
- **State:** remember/mutableStateOf (lokaler State)
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# FeatureRatingSystem
|
||||||
|
## Übersicht
|
||||||
|
Das FeatureRatingSystem enthält Komponenten zur detaillierten Anzeige von gemeldeten Straßenschäden. Es ermöglicht Nutzern, Informationen und Fotos zu einem Schaden einzusehen und diesen über ein Community-Rating-System zu validieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
## Methoden
|
||||||
|
|
||||||
|
## 1. FeatureInfoDialog
|
||||||
|
`Composable Function`
|
||||||
|
|
||||||
|
Ein modaler UI-Dialog, der zur Anzeige von Objektdaten eines `ArcGISFeature` dient.
|
||||||
|
|
||||||
|
### Zweck
|
||||||
|
Visualisierung von Sachdaten (Attribute), das asynchrone Laden von Bildanhängen und die Bereitstellung einer Schnittstelle für Nutzerinteraktionen (Bewertungen).
|
||||||
|
|
||||||
|
### Parameter
|
||||||
|
| Parameter | Typ | Beschreibung |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `feature` | `ArcGISFeature?` | Das ArcGIS-Objekt, dessen Daten angezeigt werden. |
|
||||||
|
| `onDismiss` | `() -> Unit` | Callback zum Schließen des Dialogs. |
|
||||||
|
| `onRate` | `(ArcGISFeature, Boolean) -> Unit` | Callback, der ausgelöst wird, wenn ein Nutzer eine Bewertung abgibt. |
|
||||||
|
|
||||||
|
### Interne Zustandsvariablen (State)
|
||||||
|
* **`photoBitmaps`** (`List<ImageBitmap>`): Speichert die dekodierten Bilder, die aus den Feature-Attachments geladen wurden.
|
||||||
|
* **`isLoadingPhotos`** (`Boolean`): Statusindikator, der den Ladevorgang der Anhänge steuert.
|
||||||
|
|
||||||
|
### Funktionsweise
|
||||||
|
1. **Initialisierung**: Beim Start (`LaunchedEffect`) wird geprüft, ob das Feature vollständig geladen ist.
|
||||||
|
2. **Attachment-Download**: Die Funktion ruft `fetchAttachments()` auf. Für jeden Anhang werden die Rohdaten (`fetchData`) geladen.
|
||||||
|
3. **Bildverarbeitung**: Die Byte-Arrays werden mittels `BitmapFactory` dekodiert und in `ImageBitmap` konvertiert, um sie in Compose anzuzeigen.
|
||||||
|
4. **UI-Rendering**: Die Attribute `Typ` und `Beschreibung` werden zusammen mit den Bildern in einer scrollbaren `Card` dargestellt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. updateFeatureRating
|
||||||
|
`Extension Function (suspend)`
|
||||||
|
|
||||||
|
Eine Erweiterungsfunktion für das `MapViewModel`, die die Geschäftslogik für das Bewertungssystem kapselt.
|
||||||
|
|
||||||
|
### Zweck
|
||||||
|
Persistente Aktualisierung des Community-Zählers eines Schadens in der ArcGIS Online Feature Layer Table.
|
||||||
|
|
||||||
|
### Parameter
|
||||||
|
| Parameter | Typ | Beschreibung |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `feature` | `ArcGISFeature` | Das zu bewertende Feature-Objekt. |
|
||||||
|
| `isPositive` | `Boolean` | `true` für eine Bestätigung (+1), `false` für eine Abmilderung (-1). |
|
||||||
|
| `context` | `Context` | Erforderlich für die Anzeige von UI-Feedback (Toasts). |
|
||||||
|
|
||||||
|
### Logik-Ablauf
|
||||||
|
1. **Wertberechnung**: Extrahiert das Attribut `communitycounter`. Erhöht oder verringert den Wert, wobei ein Minimum von `0` sichergestellt wird.
|
||||||
|
2. **Lokale Aktualisierung**: Setzt den neuen Wert im Attribut-Dictionary des Features und ruft `updateFeature()` auf der `ServiceFeatureTable` auf.
|
||||||
|
3. **Remote-Synchronisation**: Mittels `applyEdits()` werden die Änderungen an den ArcGIS-Server gesendet.
|
||||||
|
4. **Feedback**: Informiert den Nutzer via `Toast` und `SnackBar` über den Erfolg oder Fehler der Operation.
|
||||||
|
|
||||||
|
### Datenbank-Attribute (ArcGIS Schema)
|
||||||
|
* **`Typ`**: Identifikator für die Schadensart.
|
||||||
|
* **`Beschreibung`**: Optionaler Freitext des Erstellers.
|
||||||
|
* **`communitycounter`**: Ganzzahliger Wert zur Speicherung der Community-Validierungen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
* **Bild-Dekodierung**: Schlägt das Laden eines Bildes fehl, wird der Fehler geloggt, aber der Dialog bleibt funktionsfähig.
|
||||||
|
* **Netzwerk-Synchronisation**: Bei Fehlern während `applyEdits` wird eine Fehlermeldung ausgegeben, um den Nutzer über mangelnde Konnektivität zu informieren.
|
||||||
72
docs/ProximityNotificationService.md
Normal file
72
docs/ProximityNotificationService.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# ProximityNotificationService
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
Der `ProximityNotificationService` im Paket `com.example.snapandsolve.service` ist ein **Android Foreground Service**. Er ermöglicht die Hintergrund-Überwachung des Nutzerstandorts, um proaktiv Benachrichtigungen auszulösen, wenn sich der Nutzer in der Nähe (Standard: 100m) eines in ArcGIS registrierten Straßenschadens befindet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Architektur & Lebenszyklus
|
||||||
|
Der Dienst ist darauf ausgelegt, unabhängig von der Sichtbarkeit der App zu laufen. Als **Foreground Service** ist er mit einer permanenten System-Benachrichtigung verknüpft, um eine vorzeitige Beendigung durch das Android-Betriebssystem zu verhindern.
|
||||||
|
|
||||||
|
### Steuerung über das Companion Object
|
||||||
|
* **`start(context, featureTable)`**: Initialisiert den Dienst mit der notwendigen Datenquelle und startet ihn.
|
||||||
|
* **`stop(context)`**: Beendet das Tracking und den Dienst.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Funktionsweise des Standorts-Trackings
|
||||||
|
|
||||||
|
### Initialisierung
|
||||||
|
Nach dem Start (`onCreate`) wird die `SystemLocationDataSource` von ArcGIS initialisiert. Diese liefert kontinuierlich Standort-Updates innerhalb eines dedizierten `serviceScope` (CoroutineScope).
|
||||||
|
|
||||||
|
### Geofencing-Logik (`checkProximityToDamages`)
|
||||||
|
Bei jedem Standort-Update führt der Dienst eine räumliche Analyse durch:
|
||||||
|
|
||||||
|
1. **Abfrage**: Alle verfügbaren Features werden aus der `ServiceFeatureTable` abgerufen.
|
||||||
|
2. **Projektion**: Da GPS-Koordinaten (`SpatialReference.wgs84()`) oft nicht mit dem Koordinatensystem der Karte übereinstimmen, wird der Standort des Nutzers mittels `GeometryEngine.projectOrNull` transformiert.
|
||||||
|
3. **Distanzberechnung**: Die exakte Entfernung wird über `GeometryEngine.distanceGeodeticOrNull` berechnet. Hierbei wird der Kurventyp `Geodesic` verwendet, um die Erdkrümmung korrekt zu berücksichtigen.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Benachrichtigungs-Management
|
||||||
|
|
||||||
|
### Spam-Prävention
|
||||||
|
Um mehrfache Warnungen für denselben Schaden zu vermeiden, nutzt der Dienst ein internes Cache-System:
|
||||||
|
* **`notifiedFeatures`**: Ein Set von `OBJECTID`s, für die bereits eine Benachrichtigung gesendet wurde.
|
||||||
|
* **Hysterese-Bereinigung**: Eine ID wird erst dann aus dem Cache entfernt, wenn sich der Nutzer mehr als 150 Meter vom entsprechenden Schaden entfernt hat (`cleanupNotifiedFeatures`). Dies verhindert "flackernde" Benachrichtigungen an der Radiengrenze.
|
||||||
|
|
||||||
|
### Benachrichtigungs-Inhalt
|
||||||
|
Der Dienst sucht dynamisch nach verfügbaren Attributen im ArcGIS-Feature, um den Text zu generieren:
|
||||||
|
* **Kategorie**: Prüft Felder wie `Kategorie`, `kategorie`, `Category` oder `category`.
|
||||||
|
* **Beschreibung**: Sucht nach zusätzlichen Details in `Beschreibung` oder `Description`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Konfiguration & Konstanten
|
||||||
|
|
||||||
|
| Konstante | Wert | Beschreibung |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `PROXIMITY_RADIUS_METERS` | 100.0 | Radius, ab dem eine Warnung ausgelöst wird. |
|
||||||
|
| `CHANNEL_ID` | `proximity_notifications` | ID des Android-Benachrichtigungskanals. |
|
||||||
|
| `START_STICKY` | Konstante | Sorgt für einen automatischen Neustart des Dienstes nach einem System-Kill. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Voraussetzungen & Sicherheit
|
||||||
|
|
||||||
|
### Erforderliche Berechtigungen
|
||||||
|
In der `AndroidManifest.xml` müssen folgende Berechtigungen konfiguriert sein:
|
||||||
|
* `ACCESS_FINE_LOCATION` (Präziser Standort)
|
||||||
|
* `ACCESS_BACKGROUND_LOCATION` (Standort im Hintergrund)
|
||||||
|
* `FOREGROUND_SERVICE` (Erlaubnis für Hintergrunddienste)
|
||||||
|
|
||||||
|
### Datenintegrität
|
||||||
|
Der Dienst setzt voraus, dass die `ServiceFeatureTable` beim Start übergeben wird. Ist die Tabelle `null`, stellt der Dienst die Überprüfung ein und loggt einen Fehler, bleibt aber als Foreground Service aktiv, um Systeminstabilitäten zu vermeiden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
* **Projektionsfehler**: Falls die Koordinatentransformation fehlschlägt, wird das betroffene Feature übersprungen.
|
||||||
|
* **Geometrie-Validierung**: Nur Features mit gültigen Punkt-Geometrien werden verarbeitet.
|
||||||
57
docs/SettingsScreen.md
Normal file
57
docs/SettingsScreen.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Dokumentation: SettingsScreen (Einstellungen)
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
Die Datei `SettingsScreen.kt` im Paket `com.example.snapandsolve.ui.theme` stellt die Benutzeroberfläche für die App-Konfiguration bereit. Sie dient primär der Steuerung des **ProximityNotificationService**, welcher Nutzer benachrichtigt, sobald sie sich in der Nähe eines gemeldeten Straßenschadens befinden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Hauptkomponente: SettingsScreen
|
||||||
|
`@Composable fun SettingsScreen(onBack: () -> Unit, mapViewModel: MapViewModel)`
|
||||||
|
|
||||||
|
Ein Full-Screen Composable, das mittels Material Design 3 (M3) eine übersichtliche Struktur für Benutzereinstellungen bietet.
|
||||||
|
|
||||||
|
### Parameter
|
||||||
|
| Parameter | Typ | Beschreibung |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `onBack` | `() -> Unit` | Callback zur Navigation zurück zum vorherigen Screen. |
|
||||||
|
| `mapViewModel` | `MapViewModel` | ViewModel zur Bereitstellung der ArcGIS Feature Table. |
|
||||||
|
|
||||||
|
### Zustandsverwaltung (State)
|
||||||
|
* **`isProximityActive`**: Ein via `collectAsState` beobachteter Boolean, der den aktuellen Status des Hintergrunddienstes direkt aus dem `ProximityNotificationService` widerspiegelt.
|
||||||
|
* **`notificationPermissionLauncher`**: Ein Activity-Result-Launcher, der die erforderliche Berechtigung `POST_NOTIFICATIONS` verwaltet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Funktionslogik: Benachrichtigungs-Switch
|
||||||
|
|
||||||
|
Der zentrale Teil des Screens ist ein `Switch`, der den Proximity-Dienst steuert. Der Ablauf bei Aktivierung ist wie folgt:
|
||||||
|
|
||||||
|
1. **Versionsprüfung**: Prüft, ob die Berechtigung für Benachrichtigungen vorliegt.
|
||||||
|
2. **Berechtigungsanfrage**: Fehlt die Berechtigung, wird der System-Dialog zur Anfrage gestartet.
|
||||||
|
3. **Validierung der Datenquelle**: Es wird geprüft, ob die `FeatureTable` im `MapViewModel` verfügbar ist.
|
||||||
|
4. **Dienst-Start/Stop**:
|
||||||
|
- Bei Erfolg: `ProximityNotificationService.start(context, table)`
|
||||||
|
- Bei Deaktivierung: `ProximityNotificationService.stop(context)`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. UI-Struktur & Design
|
||||||
|
|
||||||
|
### TopAppBar
|
||||||
|
Die Kopfzeile nutzt das Farbschema der App (`AppColor`) und bietet eine konsistente Navigation.
|
||||||
|
|
||||||
|
### Layout-Elemente
|
||||||
|
* **Settings-Karten (`Card`)**: Gruppieren inhaltlich zusammenhängende Einstellungen (z.B. Benachrichtigungen, Informationen).
|
||||||
|
* **Status-Feedback**: Wenn der Dienst aktiv ist, wird dynamisch eine zusätzliche Infokarte mit grünem Häkchen (`✓`) eingeblendet, um den aktiven Status zu visualisieren.
|
||||||
|
* **Informations-Sektion**: Zeigt feste Parameter wie den Proximity-Radius (aktuell 100 Meter) an.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Integration & Anforderungen
|
||||||
|
|
||||||
|
### Erforderliche Berechtigungen
|
||||||
|
In der `AndroidManifest.xml` müssen für die volle Funktionalität dieses Screens folgende Berechtigungen deklariert sein:
|
||||||
|
```xml
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Architektur
|
|
||||||
|
|
||||||
## Überblick
|
|
||||||
|
|
||||||
## Struktur
|
|
||||||
|
|
||||||
## Verantwortlichkeiten
|
|
||||||
|
|
||||||
## Prozessablauf
|
|
||||||
```mermaid
|
|
||||||
graph TD;
|
|
||||||
A-->B;
|
|
||||||
A-->C;
|
|
||||||
B-->D;
|
|
||||||
C-->D;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Process Flow (Architecture)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
UI[UI: Screen / Fragment / Compose] -->|user action| VM[ViewModel]
|
|
||||||
VM -->|invoke| UC[Use Case]
|
|
||||||
UC -->|calls| R[Repository]
|
|
||||||
R -->|read/write| LDS[Local Data Source\nDB / DataStore]
|
|
||||||
R -->|fetch| RDS[Remote Data Source\nREST / GraphQL]
|
|
||||||
RDS -->|DTOs| MAP[Mapper]
|
|
||||||
LDS -->|Entities| MAP
|
|
||||||
MAP -->|Domain Model| UC
|
|
||||||
UC -->|Result| VM
|
|
||||||
VM -->|StateFlow / LiveData| UI
|
|
||||||
|
|
||||||
subgraph Data
|
|
||||||
R
|
|
||||||
LDS
|
|
||||||
RDS
|
|
||||||
MAP
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# locationHelper
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
Dieses Modul stellt Hilfsfunktionen und Composables bereit, um die GPS-Standortbestimmung innerhalb der ArcGIS Maps SDK für Kotlin (Compose-Toolkit) zu verwalten. Es kümmert sich um die Prüfung und Abfrage von Android-Laufzeitberechtigungen sowie die Initialisierung des `LocationDisplay`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. LocationHelper (Klasse)
|
||||||
|
`class LocationHelper(private val context: Context)`
|
||||||
|
|
||||||
|
Eine Utility-Klasse zur Kapselung von Berechtigungsprüfungen.
|
||||||
|
|
||||||
|
### Zweck
|
||||||
|
Zentralisierung der Logik für die Prüfung von Standortberechtigungen (`ACCESS_COARSE_LOCATION` und `ACCESS_FINE_LOCATION`).
|
||||||
|
|
||||||
|
### Methoden
|
||||||
|
| Methode | Rückgabetyp | Beschreibung |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `hasLocationPermissions()` | `Boolean` | Gibt `true` zurück, wenn sowohl die grobe als auch die feine Standortberechtigung vom Nutzer erteilt wurde. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. setupLocationDisplay (Composable)
|
||||||
|
`@Composable fun setupLocationDisplay(autoPanMode: LocationDisplayAutoPanMode): LocationDisplay`
|
||||||
|
|
||||||
|
Die Haupt-Einstiegsfunktion für die Standortvisualisierung in einer MapView.
|
||||||
|
|
||||||
|
### Zweck
|
||||||
|
Initialisiert das `LocationDisplay`-Objekt, setzt den Modus für die automatische Schwenkung der Karte (Auto-Pan) und startet die Datenquelle für Standortaktualisierungen.
|
||||||
|
|
||||||
|
### Parameter
|
||||||
|
| Parameter | Typ | Default | Beschreibung |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `autoPanMode` | `LocationDisplayAutoPanMode` | `.Recenter` | Bestimmt das Verhalten der Kamera bei Standortänderung (z.B. Zentrieren oder Navigieren). |
|
||||||
|
|
||||||
|
### Funktionsweise
|
||||||
|
1. **Initialisierung**: Erzeugt ein `LocationDisplay` mittels `rememberLocationDisplay()`.
|
||||||
|
2. **Berechtigungsprüfung**: Nutzt den `LocationHelper`, um den aktuellen Status zu prüfen.
|
||||||
|
3. **Datenquelle starten**:
|
||||||
|
- Sind Berechtigungen vorhanden: Startet die `dataSource` sofort via `LaunchedEffect`.
|
||||||
|
- Fehlen Berechtigungen: Ruft das Composable `RequestLocationPermissions` auf.
|
||||||
|
4. **Rückgabe**: Liefert das konfigurierte Objekt an die übergeordnete `MapView` zurück.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. RequestLocationPermissions (Privates Composable)
|
||||||
|
`@Composable private fun RequestLocationPermissions(...)`
|
||||||
|
|
||||||
|
Ein UI-Komponente zur Interaktion mit dem Android-Berechtigungssystem.
|
||||||
|
|
||||||
|
### Zweck
|
||||||
|
Anforderung der erforderlichen Berechtigungen während der Laufzeit (Runtime Permissions).
|
||||||
|
|
||||||
|
### Parameter
|
||||||
|
| Parameter | Typ | Beschreibung |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `context` | `Context` | Android-Kontext für Toast-Meldungen. |
|
||||||
|
| `onPermissionsGranted` | `() -> Unit` | Callback, der ausgeführt wird, wenn der Nutzer alle angeforderten Rechte bestätigt hat. |
|
||||||
|
|
||||||
|
### Ablauf
|
||||||
|
1. Nutzt `rememberLauncherForActivityResult`, um auf die Antwort des Betriebssystems zu warten.
|
||||||
|
2. Fordert im `LaunchedEffect` gleichzeitig `ACCESS_COARSE_LOCATION` und `ACCESS_FINE_LOCATION` an.
|
||||||
|
3. **Erfolg**: Ruft `onPermissionsGranted()` auf, was in der Regel den Start des GPS-Tracking auslöst.
|
||||||
|
4. **Ablehnung**: Zeigt eine `Toast`-Meldung an, um den Nutzer über die fehlende Funktionalität aufzuklären.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwendete Berechtigungen (Manifest)
|
||||||
|
Für die korrekte Funktion müssen folgende Tags in der `AndroidManifest.xml` vorhanden sein:
|
||||||
|
* `android.permission.ACCESS_FINE_LOCATION`
|
||||||
|
* `android.permission.ACCESS_COARSE_LOCATION`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architektur-Hinweis
|
||||||
|
Das Modul nutzt das **ArcGIS Maps Compose Toolkit**. Das zurückgegebene `LocationDisplay` wird normalerweise direkt in einer `MapView` Composable als Parameter übergeben:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
MapView(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
arcGISMap = map,
|
||||||
|
locationDisplay = setupLocationDisplay()
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user