diff --git a/app/src/main/java/com/example/snapandsolve/camera/AlbumViewState.kt b/app/src/main/java/com/example/snapandsolve/camera/AlbumViewState.kt index 3c7dd70..a2c64c1 100644 --- a/app/src/main/java/com/example/snapandsolve/camera/AlbumViewState.kt +++ b/app/src/main/java/com/example/snapandsolve/camera/AlbumViewState.kt @@ -8,12 +8,12 @@ import androidx.compose.ui.graphics.ImageBitmap */ 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, /** - * 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 = emptyList(), ) \ No newline at end of file diff --git a/app/src/main/java/com/example/snapandsolve/service/ProximityNotificationService.kt b/app/src/main/java/com/example/snapandsolve/service/ProximityNotificationService.kt index 54e6db4..8366291 100644 --- a/app/src/main/java/com/example/snapandsolve/service/ProximityNotificationService.kt +++ b/app/src/main/java/com/example/snapandsolve/service/ProximityNotificationService.kt @@ -167,7 +167,7 @@ class ProximityNotificationService : Service() { 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) { GeometryEngine.projectOrNull(currentPoint, featureGeometry.spatialReference!!) as? Point } else { @@ -179,7 +179,7 @@ class ProximityNotificationService : Service() { return@forEach } - // KORRIGIERTE Distanzberechnung mit GeometryEngine + // korrigierte Distanzberechnung mit GeometryEngine val distanceResult = GeometryEngine.distanceGeodeticOrNull( point1 = projectedCurrentPoint, point2 = featureGeometry, @@ -214,14 +214,14 @@ class ProximityNotificationService : Service() { } private fun sendDamageNotification(feature: ArcGISFeature, distance: Double) { - // Versuche verschiedene Attribut-Namen für die Kategorie + val kategorie = feature.attributes["Kategorie"]?.toString() ?: feature.attributes["kategorie"]?.toString() ?: feature.attributes["Category"]?.toString() ?: feature.attributes["category"]?.toString() ?: "Straßenschaden" // Fallback - // Versuche verschiedene Attribut-Namen für die Beschreibung + val beschreibung = feature.attributes["Beschreibung"]?.toString() ?: feature.attributes["beschreibung"]?.toString() ?: feature.attributes["Description"]?.toString() @@ -229,7 +229,7 @@ class ProximityNotificationService : Service() { val distanceText = String.format("%.0f", distance) - // Baue den Notification-Text + // Notification-Text val notificationText = if (beschreibung != null && beschreibung.isNotEmpty()) { "$kategorie: $beschreibung - ca. ${distanceText}m entfernt" } else { diff --git a/app/src/main/java/com/example/snapandsolve/ui/theme/DamageFilterSystem.kt b/app/src/main/java/com/example/snapandsolve/ui/theme/DamageFilterSystem.kt index 8681d2d..acbcbbd 100644 --- a/app/src/main/java/com/example/snapandsolve/ui/theme/DamageFilterSystem.kt +++ b/app/src/main/java/com/example/snapandsolve/ui/theme/DamageFilterSystem.kt @@ -67,7 +67,7 @@ fun DamageFilterDialog( // Status-Liste val statusTypes = listOf("neu", "in Bearbeitung", "Schaden behoben") - // WICHTIG: Wenn keine Filter aktiv sind, alle Typen standardmäßig auswählen! + var selectedFilters by remember { mutableStateOf( if (currentFilters.isEmpty()) damageTypes.toSet() else currentFilters @@ -120,7 +120,7 @@ fun DamageFilterDialog( .weight(1f) .verticalScroll(rememberScrollState()) ) { - // ===== TYP-FILTER ===== + // Filtertyp Text( text = "Schadenstypen:", style = MaterialTheme.typography.titleMedium, @@ -153,7 +153,7 @@ fun DamageFilterDialog( Divider(modifier = Modifier.padding(vertical = 16.dp)) - // ===== STATUS-FILTER ===== + // Filter nach Status Text( text = "Bearbeitungsstatus:", style = MaterialTheme.typography.titleMedium, @@ -190,7 +190,7 @@ fun DamageFilterDialog( Divider(modifier = Modifier.padding(vertical = 16.dp)) - // ===== DATUMS-FILTER ===== + // Filter nach Datum Row( modifier = Modifier .fillMaxWidth() @@ -364,8 +364,8 @@ fun DamageFilterDialog( } /** - * Extension-Funktion für MapViewModel - * Wendet Filter auf den FeatureLayer an (Typ + Status + Datum - UNABHÄNGIG) + * Erweiterungs-Funktion für MapViewModel + * Wendet Filter auf den FeatureLayer an */ suspend fun MapViewModel.applyDamageFilter( selectedTypes: Set, @@ -379,22 +379,22 @@ suspend fun MapViewModel.applyDamageFilter( try { val whereClauses = mutableListOf() - // ===== TYP-FILTER ===== - // WICHTIG: Nur hinzufügen wenn nicht ALLE Typen ausgewählt sind + // Filter Typ + if (selectedTypes.isNotEmpty() && selectedTypes.size < DAMAGE_TYPES.size) { val typeList = selectedTypes.joinToString("', '", "'", "'") whereClauses.add("Typ IN ($typeList)") } - // ===== STATUS-FILTER ===== - // Nur hinzufügen wenn nicht ALLE Status ausgewählt sind + // Filter Status + if (selectedStatus.isNotEmpty() && selectedStatus.size < statusTypes.size) { val statusList = selectedStatus.joinToString("', '", "'", "'") whereClauses.add("status IN ($statusList)") } - // ===== DATUMS-FILTER ===== - // Feldname: EditDate + // Filter nach Datum + if (startDate != null || endDate != null) { val dateField = "EditDate" @@ -411,7 +411,7 @@ suspend fun MapViewModel.applyDamageFilter( } } - // ===== ALLE KLAUSELN KOMBINIEREN ===== + // Alle Klauseln kombinieren val whereClause = whereClauses.joinToString(" AND ") 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 */ fun MapViewModel.getActiveFilters(): Set { diff --git a/app/src/main/java/com/example/snapandsolve/ui/theme/DamageListSystem.kt b/app/src/main/java/com/example/snapandsolve/ui/theme/DamageListSystem.kt index 837d981..bbb8955 100644 --- a/app/src/main/java/com/example/snapandsolve/ui/theme/DamageListSystem.kt +++ b/app/src/main/java/com/example/snapandsolve/ui/theme/DamageListSystem.kt @@ -152,7 +152,7 @@ fun DamageListDialog( Divider(modifier = Modifier.padding(vertical = 12.dp)) - // ===== ENTFERNUNGS-FILTER ===== + // Filtern nach Entfernung Column(modifier = Modifier.fillMaxWidth()) { Row( modifier = Modifier.fillMaxWidth(), @@ -192,7 +192,7 @@ fun DamageListDialog( Spacer(modifier = Modifier.height(16.dp)) - // ===== SORTIERUNG ===== + // Sortieren Column(modifier = Modifier.fillMaxWidth()) { Text( text = "Sortieren nach:", @@ -205,7 +205,7 @@ fun DamageListDialog( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - // Distance Button + // Entfernung Button FilterChip( selected = sortBy == SortBy.DISTANCE, onClick = { sortBy = SortBy.DISTANCE }, @@ -220,7 +220,7 @@ fun DamageListDialog( modifier = Modifier.weight(1f) ) - // Relevance Button + // Relevanz Button FilterChip( selected = sortBy == SortBy.RELEVANCE, onClick = { sortBy = SortBy.RELEVANCE }, @@ -239,7 +239,7 @@ fun DamageListDialog( Spacer(modifier = Modifier.height(16.dp)) - // ===== RELEVANZ-FILTER (nur wenn nach Relevanz sortiert) ===== + // Nach Relevanz filtern if (sortBy == SortBy.RELEVANCE) { Column(modifier = Modifier.fillMaxWidth()) { 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 fun DamageListItemWithPhoto( @@ -401,7 +401,7 @@ fun DamageListItemWithPhoto( modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically ) { - // LINKS: Foto + //LINKS: Foto Box( modifier = Modifier .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 */ suspend fun MapViewModel.loadDamagesNearby( diff --git a/app/src/main/java/com/example/snapandsolve/ui/theme/FeatureRatingSystem.kt b/app/src/main/java/com/example/snapandsolve/ui/theme/FeatureRatingSystem.kt index 6a52810..2459881 100644 --- a/app/src/main/java/com/example/snapandsolve/ui/theme/FeatureRatingSystem.kt +++ b/app/src/main/java/com/example/snapandsolve/ui/theme/FeatureRatingSystem.kt @@ -19,7 +19,6 @@ 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.Attachment import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -35,12 +34,13 @@ fun FeatureInfoDialog( ) { if (feature == null) return - var attachments by remember { mutableStateOf>(emptyList()) } + var photoBitmaps by remember { mutableStateOf>(emptyList()) } var isLoadingPhotos by remember { mutableStateOf(true) } // Lade Fotos beim Öffnen LaunchedEffect(feature) { + isLoadingPhotos = true try { // Lade Feature falls nötig if (feature.loadStatus.value != com.arcgismaps.LoadStatus.Loaded) { @@ -49,23 +49,22 @@ fun FeatureInfoDialog( // Hole Attachments feature.fetchAttachments().onSuccess { fetchedAttachments -> - attachments = fetchedAttachments + val loadedBitmaps = mutableListOf() - // Lade Foto-Daten - val bitmaps = mutableListOf() fetchedAttachments.forEach { attachment -> attachment.fetchData().onSuccess { data -> try { val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size) 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) { - println("DEBUG: Fehler beim Laden des Bildes: ${e.message}") + println("DEBUG: Fehler beim Decodieren des Bildes: ${e.message}") } } } - photoBitmaps = bitmaps } } catch (e: Exception) { println("DEBUG: Fehler beim Laden der Attachments: ${e.message}") @@ -95,7 +94,7 @@ fun FeatureInfoDialog( modifier = Modifier.padding(bottom = 16.dp) ) - Divider(modifier = Modifier.padding(bottom = 16.dp)) + HorizontalDivider(modifier = Modifier.padding(bottom = 16.dp)) // Beschreibung val description = feature.attributes["Beschreibung"]?.toString() @@ -113,7 +112,7 @@ fun FeatureInfoDialog( } // Fotos - if (isLoadingPhotos) { + if (isLoadingPhotos && photoBitmaps.isEmpty()) { Row( modifier = Modifier .fillMaxWidth() @@ -122,7 +121,7 @@ fun FeatureInfoDialog( ) { CircularProgressIndicator(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.width(8.dp)) - Text("Lade Fotos...") + Text("Suche Fotos...") } } else if (photoBitmaps.isNotEmpty()) { Text( @@ -149,7 +148,6 @@ fun FeatureInfoDialog( ) } } - Spacer(modifier = Modifier.height(8.dp)) } @@ -200,7 +198,6 @@ fun FeatureInfoDialog( } } - // Info-Text Text( text = "Hast du diesen Schaden auch gesehen?", style = MaterialTheme.typography.bodyMedium, @@ -211,24 +208,19 @@ fun FeatureInfoDialog( Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - // Daumen runter - OutlinedButton( + ) {//Daumen runter + Button( onClick = { onRate(feature, false) onDismiss() }, modifier = Modifier.weight(1f) ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(vertical = 8.dp) - ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("👎", style = MaterialTheme.typography.headlineMedium) Text("Nein", style = MaterialTheme.typography.bodyMedium) } } - // Daumen hoch Button( onClick = { @@ -237,17 +229,13 @@ fun FeatureInfoDialog( }, modifier = Modifier.weight(1f) ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(vertical = 8.dp) - ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("👍", style = MaterialTheme.typography.headlineMedium) Text("Ja", style = MaterialTheme.typography.bodyMedium) } } } - // Schließen Button TextButton( onClick = onDismiss, modifier = Modifier @@ -261,10 +249,8 @@ fun FeatureInfoDialog( } } - - /** - * Extension-Funktion für MapViewModel + * Erweiterungs-Funktion für MapViewModel * Aktualisiert die Community-Bewertung eines Features */ suspend fun MapViewModel.updateFeatureRating( @@ -274,62 +260,31 @@ suspend fun MapViewModel.updateFeatureRating( ): Boolean { return withContext(Dispatchers.IO) { try { - // Hole aktuelle Bewertung 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 - // Speichere in Feature Table serviceFeatureTable.updateFeature(feature).onSuccess { - println("DEBUG: updateFeature erfolgreich") - - // Synchronisiere mit ArcGIS Online serviceFeatureTable.applyEdits().onSuccess { - println("DEBUG: applyEdits erfolgreich - Rating gespeichert") - withContext(Dispatchers.Main) { - val message = if (isPositive) { - "✓ Schaden bestätigt! (${currentRating} → ${newRating})" - } else { - "✓ Bewertung verringert (${currentRating} → ${newRating})" - } + val message = if (isPositive) "✓ Schaden bestätigt!" else "✓ Bewertung verringert" Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - snackBarMessage = message } }.onFailure { error -> - println("DEBUG: applyEdits fehlgeschlagen: ${error.message}") withContext(Dispatchers.Main) { Toast.makeText(context, "Sync-Fehler: ${error.message}", Toast.LENGTH_LONG).show() } } }.onFailure { error -> - println("DEBUG: updateFeature fehlgeschlagen: ${error.message}") withContext(Dispatchers.Main) { Toast.makeText(context, "Update-Fehler: ${error.message}", Toast.LENGTH_LONG).show() } } - true } catch (e: Exception) { - println("DEBUG: Rating-Update Exception: ${e.message}") - e.printStackTrace() - withContext(Dispatchers.Main) { - Toast.makeText( - context, - "Fehler beim Bewerten: ${e.message}", - Toast.LENGTH_LONG - ).show() + Toast.makeText(context, "Fehler: ${e.message}", Toast.LENGTH_LONG).show() } false } diff --git a/app/src/main/java/com/example/snapandsolve/ui/theme/SettingsScreen.kt b/app/src/main/java/com/example/snapandsolve/ui/theme/SettingsScreen.kt index b3464ef..32e2ce4 100644 --- a/app/src/main/java/com/example/snapandsolve/ui/theme/SettingsScreen.kt +++ b/app/src/main/java/com/example/snapandsolve/ui/theme/SettingsScreen.kt @@ -28,7 +28,7 @@ fun SettingsScreen( val context = LocalContext.current val isProximityActive by ProximityNotificationService.isRunning.collectAsState() - // NEU: Notification Permission Launcher + // Notification Permission Launcher val notificationPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted -> @@ -65,7 +65,7 @@ fun SettingsScreen( .padding(paddingValues) .padding(16.dp) ) { - // Benachrichtigungen Section + // Benachrichtigungen Text( text = "Benachrichtigungen", style = MaterialTheme.typography.titleMedium, @@ -180,7 +180,7 @@ fun SettingsScreen( Spacer(modifier = Modifier.height(24.dp)) - // Weitere Einstellungen können hier hinzugefügt werden + Text( text = "Informationen", style = MaterialTheme.typography.titleMedium, diff --git a/app/src/main/java/com/example/snapandsolve/ui/theme/locationHelper.kt b/app/src/main/java/com/example/snapandsolve/ui/theme/locationHelper.kt index e903832..090b3c9 100644 --- a/app/src/main/java/com/example/snapandsolve/ui/theme/locationHelper.kt +++ b/app/src/main/java/com/example/snapandsolve/ui/theme/locationHelper.kt @@ -43,8 +43,6 @@ class LocationHelper(private val context: Context) { /** * 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 fun setupLocationDisplay( diff --git a/docs/AlbumViewModel.md b/docs/AlbumViewModel.md index db1f741..5194d60 100644 --- a/docs/AlbumViewModel.md +++ b/docs/AlbumViewModel.md @@ -1,40 +1,77 @@ -# Album/Kamera-System Dokumentation +# AlbumViewModel -## AlbumViewModel - -```kotlin -class AlbumViewModel(private val coroutineContext: CoroutineContext) : ViewModel() -``` - -**Zweck:** Verwaltung von Bildauswahl und Kamera-Aufnahmen für Schadensmeldungen. - -### Properties - -| Name | Typ | Beschreibung | -|------|-----|--------------| -| `viewStateFlow` | `StateFlow` | 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)` - 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. +## Ü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. --- +## 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` +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. \ No newline at end of file diff --git a/docs/DamageFilterSystem.md b/docs/DamageFilterSystem.md index e69de29..16f6ea3 100644 --- a/docs/DamageFilterSystem.md +++ b/docs/DamageFilterSystem.md @@ -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, + currentFilters: Set, + onDismiss: () -> Unit, + onApplyFilter: (Set, Set, LocalDate?, LocalDate?) -> Unit +) +``` + +**Zweck:** Vollbildschirm-Dialog zur Auswahl von Filter-Kriterien. + +**Parameter:** + +| Name | Typ | Beschreibung | +|------|-----|--------------| +| `damageTypes` | List | Verfügbare Schadenstypen (z.B. "Straße", "Gehweg") | +| `currentFilters` | Set | Aktuell aktive Typ-Filter | +| `onDismiss` | () -> Unit | Callback zum Schließen des Dialogs | +| `onApplyFilter` | (Set, Set, 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 | alle Typen | Ausgewählte Schadenstypen | +| `selectedStatus` | Set | 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, + selectedStatus: Set, + startDate: LocalDate? = null, + endDate: LocalDate? = null +): Boolean +``` + +**Zweck:** Wendet Filter auf FeatureLayer via SQL WHERE-Clause an. + +**Parameter:** + +| Name | Typ | Beschreibung | +|------|-----|--------------| +| `selectedTypes` | Set | Ausgewählte Schadenstypen | +| `selectedStatus` | Set | 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 +``` + +**Zweck:** Extrahiert aktuell aktive Filter aus FeatureLayer.definitionExpression. + +**Return:** Set - 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) diff --git a/docs/DamageListSystem.md b/docs/DamageListSystem.md index e69de29..d2d0533 100644 --- a/docs/DamageListSystem.md +++ b/docs/DamageListSystem.md @@ -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 | 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 +``` + +**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 - 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) diff --git a/docs/FeatureRatingSystem.md b/docs/FeatureRatingSystem.md index e69de29..f5148c4 100644 --- a/docs/FeatureRatingSystem.md +++ b/docs/FeatureRatingSystem.md @@ -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`): 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. \ No newline at end of file diff --git a/docs/ProximityNotificationService.md b/docs/ProximityNotificationService.md new file mode 100644 index 0000000..c39346b --- /dev/null +++ b/docs/ProximityNotificationService.md @@ -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. \ No newline at end of file diff --git a/docs/SettingsScreen.md b/docs/SettingsScreen.md new file mode 100644 index 0000000..a143b12 --- /dev/null +++ b/docs/SettingsScreen.md @@ -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 + \ No newline at end of file diff --git a/docs/Widget.md b/docs/Widget.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 1d7ec34..0000000 --- a/docs/architecture.md +++ /dev/null @@ -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 diff --git a/docs/locationHelper.md b/docs/locationHelper.md index e69de29..2cfd105 100644 --- a/docs/locationHelper.md +++ b/docs/locationHelper.md @@ -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() +) \ No newline at end of file