- Bewertungsystem
- Filtern nach Schäden - Docs Ordner mit Markdown dummies gefüllt.
This commit is contained in:
@@ -24,6 +24,7 @@ import androidx.compose.material.icons.filled.AddLocationAlt
|
|||||||
import androidx.compose.material.icons.filled.FilterAlt
|
import androidx.compose.material.icons.filled.FilterAlt
|
||||||
import androidx.compose.material.icons.filled.FormatListNumbered
|
import androidx.compose.material.icons.filled.FormatListNumbered
|
||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material.icons.filled.ThumbUp
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
@@ -48,6 +49,8 @@ import com.example.snapandsolve.camera.AlbumViewState
|
|||||||
import com.example.snapandsolve.camera.Intent
|
import com.example.snapandsolve.camera.Intent
|
||||||
import com.example.snapandsolve.ui.theme.*
|
import com.example.snapandsolve.ui.theme.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(modifier: Modifier = Modifier, application: Application) {
|
fun MainScreen(modifier: Modifier = Modifier, application: Application) {
|
||||||
@@ -124,15 +127,20 @@ fun ContentScreen(
|
|||||||
sliderOpen: Boolean,
|
sliderOpen: Boolean,
|
||||||
onDismissReport: () -> Unit
|
onDismissReport: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// NEU: State für Filter-Dialog
|
||||||
|
var showFilterDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
// HINTERGRUND: Die Map
|
// Map
|
||||||
MapSegment(
|
MapSegment(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
mapViewModel = mapViewModel,
|
mapViewModel = mapViewModel
|
||||||
)
|
)
|
||||||
|
|
||||||
// VORDERGRUND: Das Overlay (wenn showReport = true)
|
// Report Overlay
|
||||||
if (showReport) {
|
if (showReport) {
|
||||||
ReportOverlay(
|
ReportOverlay(
|
||||||
onCancel = onDismissReport,
|
onCancel = onDismissReport,
|
||||||
@@ -142,15 +150,34 @@ fun ContentScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mapViewModel.showFeatureInfo && mapViewModel.selectedFeature != null) {
|
// Feature Info Dialog
|
||||||
FeatureInfoOverlay(
|
if (mapViewModel.showFeatureInfo) {
|
||||||
feature = mapViewModel.selectedFeature!!,
|
FeatureInfoDialog(
|
||||||
onClose = { mapViewModel.closeFeatureInfo() }
|
feature = mapViewModel.selectedFeature,
|
||||||
|
onDismiss = { mapViewModel.closeFeatureInfo() },
|
||||||
|
onRate = { feature, isPositive ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
mapViewModel.updateFeatureRating(feature, isPositive, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter Dialog - NEU!
|
||||||
|
if (showFilterDialog) {
|
||||||
|
DamageFilterDialog(
|
||||||
|
damageTypes = MapViewModel.DAMAGE_TYPES, // <-- Nutzt zentrale Liste
|
||||||
|
currentFilters = mapViewModel.getActiveFilters(),
|
||||||
|
onDismiss = { showFilterDialog = false },
|
||||||
|
onApplyFilter = { selectedTypes ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
mapViewModel.applyDamageFilter(selectedTypes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Slider von Links
|
// Side Slider
|
||||||
SideSlider(visible = sliderOpen) {
|
SideSlider(visible = sliderOpen) {
|
||||||
Text(
|
Text(
|
||||||
"Menü",
|
"Menü",
|
||||||
@@ -162,16 +189,14 @@ fun ContentScreen(
|
|||||||
text = "Schäden filtern",
|
text = "Schäden filtern",
|
||||||
icon = Icons.Default.FilterAlt,
|
icon = Icons.Default.FilterAlt,
|
||||||
onClick = {
|
onClick = {
|
||||||
/* TODO */
|
showFilterDialog = true // <-- Öffnet Filter-Dialog
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
SliderMenuItem(
|
SliderMenuItem(
|
||||||
text = "Schadensliste",
|
text = "Schadensliste",
|
||||||
icon = Icons.Default.FormatListNumbered,
|
icon = Icons.Default.FormatListNumbered,
|
||||||
onClick = {
|
onClick = { /* TODO */ }
|
||||||
/* TODO */
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,7 +300,7 @@ fun ReportOverlay(
|
|||||||
expanded = dropdownExpanded,
|
expanded = dropdownExpanded,
|
||||||
onDismissRequest = { dropdownExpanded = false }
|
onDismissRequest = { dropdownExpanded = false }
|
||||||
) {
|
) {
|
||||||
listOf("Straße", "Gehweg", "Fahrradweg", "Beleuchtung", "Sonstiges").forEach { typ ->
|
MapViewModel.DAMAGE_TYPES.forEach { typ -> // <-- Nutzt zentrale Liste
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(typ) },
|
text = { Text(typ) },
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|||||||
@@ -9,10 +9,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -20,8 +18,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.arcgismaps.ApiKey
|
import com.arcgismaps.ApiKey
|
||||||
import com.arcgismaps.ArcGISEnvironment
|
import com.arcgismaps.ArcGISEnvironment
|
||||||
import com.arcgismaps.Color
|
|
||||||
import com.arcgismaps.data.ArcGISFeature
|
|
||||||
import com.arcgismaps.location.LocationDisplayAutoPanMode
|
import com.arcgismaps.location.LocationDisplayAutoPanMode
|
||||||
import com.arcgismaps.toolkit.geoviewcompose.MapView
|
import com.arcgismaps.toolkit.geoviewcompose.MapView
|
||||||
import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
|
import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
|
||||||
@@ -31,6 +27,7 @@ import kotlinx.coroutines.launch
|
|||||||
fun MapSegment(
|
fun MapSegment(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
mapViewModel: MapViewModel
|
mapViewModel: MapViewModel
|
||||||
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
@@ -64,8 +61,7 @@ fun MapSegment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier.fillMaxSize(),
|
||||||
.fillMaxSize(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
MapView(
|
MapView(
|
||||||
@@ -73,11 +69,9 @@ fun MapSegment(
|
|||||||
arcGISMap = mapViewModel.map,
|
arcGISMap = mapViewModel.map,
|
||||||
locationDisplay = locationDisplay,
|
locationDisplay = locationDisplay,
|
||||||
mapViewProxy = mapViewModel.mapViewProxy,
|
mapViewProxy = mapViewModel.mapViewProxy,
|
||||||
onSingleTapConfirmed = mapViewModel::onTap,
|
onSingleTapConfirmed = mapViewModel::onTap, // Ganz normal
|
||||||
graphicsOverlays = listOf(mapViewModel.tempOverlay)
|
graphicsOverlays = listOf(mapViewModel.tempOverlay)
|
||||||
) {
|
)
|
||||||
/* TODO */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,4 +119,4 @@ fun RequestPermissions(context: Context, onPermissionsGranted: () -> Unit) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,11 +33,21 @@ import java.io.ByteArrayOutputStream
|
|||||||
|
|
||||||
|
|
||||||
class MapViewModel(application: Application) : AndroidViewModel(application) {
|
class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Zentrale Definition der Schadenstypen
|
||||||
|
val DAMAGE_TYPES = listOf(
|
||||||
|
"Straße",
|
||||||
|
"Gehweg",
|
||||||
|
"Fahrradweg",
|
||||||
|
"Beleuchtung",
|
||||||
|
"Sonstiges"
|
||||||
|
)
|
||||||
|
}
|
||||||
val map: ArcGISMap = ArcGISMap(BasemapStyle.OpenOsmStyle).apply {
|
val map: ArcGISMap = ArcGISMap(BasemapStyle.OpenOsmStyle).apply {
|
||||||
initialViewpoint = Viewpoint(53.14, 8.20, 20000.0)
|
initialViewpoint = Viewpoint(53.14, 8.20, 20000.0)
|
||||||
}
|
}
|
||||||
var selectedOperation by mutableStateOf(FeatureOperationType.DEFAULT)
|
var selectedOperation by mutableStateOf(FeatureOperationType.DEFAULT)
|
||||||
private set
|
|
||||||
var reopenReport by mutableStateOf(false)
|
var reopenReport by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
var showFeatureInfo by mutableStateOf(false)
|
var showFeatureInfo by mutableStateOf(false)
|
||||||
@@ -64,7 +74,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
init {
|
init {
|
||||||
tempOverlay.graphics.add(pointGraphic)
|
tempOverlay.graphics.add(pointGraphic)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/arcgis/rest/services/251120_StrassenSchaeden/FeatureServer/0")
|
serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/arcgis/rest/services/si_StrassenSchaeden/FeatureServer/0")
|
||||||
serviceFeatureTable.load().onSuccess {
|
serviceFeatureTable.load().onSuccess {
|
||||||
val typeDamageField = serviceFeatureTable.fields.firstOrNull { it.name == "Typ" }
|
val typeDamageField = serviceFeatureTable.fields.firstOrNull { it.name == "Typ" }
|
||||||
val attributeDomain = typeDamageField?.domain as? CodedValueDomain
|
val attributeDomain = typeDamageField?.domain as? CodedValueDomain
|
||||||
@@ -175,7 +185,6 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onTap(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
|
fun onTap(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
|
||||||
|
|
||||||
if (featureLayer.loadStatus.value != LoadStatus.Loaded) {
|
if (featureLayer.loadStatus.value != LoadStatus.Loaded) {
|
||||||
snackBarMessage = "Layer not loaded!"
|
snackBarMessage = "Layer not loaded!"
|
||||||
return
|
return
|
||||||
@@ -187,7 +196,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
FeatureOperationType.UPDATE_ATTRIBUTE -> selectFeatureForAttributeEditAt(singleTapConfirmedEvent.screenCoordinate)
|
FeatureOperationType.UPDATE_ATTRIBUTE -> selectFeatureForAttributeEditAt(singleTapConfirmedEvent.screenCoordinate)
|
||||||
FeatureOperationType.UPDATE_GEOMETRY -> updateFeatureGeometryAt(singleTapConfirmedEvent.screenCoordinate)
|
FeatureOperationType.UPDATE_GEOMETRY -> updateFeatureGeometryAt(singleTapConfirmedEvent.screenCoordinate)
|
||||||
FeatureOperationType.PICK_REPORT_LOCATION -> pickReportLocation(singleTapConfirmedEvent.screenCoordinate)
|
FeatureOperationType.PICK_REPORT_LOCATION -> pickReportLocation(singleTapConfirmedEvent.screenCoordinate)
|
||||||
else -> {}
|
// RATE_FEATURE wird nicht mehr gebraucht - wird im DEFAULT mit behandelt!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +259,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// When a feature is selected, update its geometry to the tapped location.
|
|
||||||
else -> {
|
else -> {
|
||||||
mapViewProxy.screenToLocationOrNull(screenCoordinate)?.let { mapPoint ->
|
mapViewProxy.screenToLocationOrNull(screenCoordinate)?.let { mapPoint ->
|
||||||
// Normalize the point - needed when the tapped location is over the international date line.
|
// Normalize the point - needed when the tapped location is over the international date line.
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package com.example.snapandsolve.ui.theme
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import MapViewModel
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog für Schaden-Filter
|
||||||
|
* Ermöglicht das Filtern von Features nach Typ
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DamageFilterDialog(
|
||||||
|
damageTypes: List<String>,
|
||||||
|
currentFilters: Set<String>,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onApplyFilter: (Set<String>) -> Unit
|
||||||
|
) {
|
||||||
|
var selectedFilters by remember { mutableStateOf(currentFilters) }
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.7f),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(20.dp)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Schäden filtern",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Schließen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
|
||||||
|
// Info-Text
|
||||||
|
Text(
|
||||||
|
text = "Wähle die Schadenstypen aus, die angezeigt werden sollen:",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter-Liste
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
damageTypes.forEach { type ->
|
||||||
|
FilterCheckboxItem(
|
||||||
|
label = type,
|
||||||
|
isChecked = selectedFilters.contains(type),
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
selectedFilters = if (isChecked) {
|
||||||
|
selectedFilters + type
|
||||||
|
} else {
|
||||||
|
selectedFilters - type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
|
||||||
|
// Info über aktive Filter
|
||||||
|
if (selectedFilters.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "${selectedFilters.size} von ${damageTypes.size} Typen ausgewählt",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// Alle auswählen
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { selectedFilters = damageTypes.toSet() },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Alle")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle abwählen
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { selectedFilters = emptySet() },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Keine")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Anwenden Button
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onApplyFilter(selectedFilters)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
if (selectedFilters.isEmpty()) "Alle anzeigen"
|
||||||
|
else "Filter anwenden"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkbox-Item für einen Filter
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun FilterCheckboxItem(
|
||||||
|
label: String,
|
||||||
|
isChecked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = isChecked,
|
||||||
|
onCheckedChange = onCheckedChange
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = when (label) {
|
||||||
|
"Straße" -> "🛣️"
|
||||||
|
"Gehweg" -> "🚶"
|
||||||
|
"Fahrradweg" -> "🚴"
|
||||||
|
"Beleuchtung" -> "💡"
|
||||||
|
"Sonstiges" -> "📍"
|
||||||
|
else -> "•"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension-Funktion für MapViewModel
|
||||||
|
* Wendet Filter auf den FeatureLayer an
|
||||||
|
*/
|
||||||
|
suspend fun MapViewModel.applyDamageFilter(selectedTypes: Set<String>): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
if (selectedTypes.isEmpty()) {
|
||||||
|
// Kein Filter - zeige alle Features
|
||||||
|
featureLayer.definitionExpression = ""
|
||||||
|
println("DEBUG: Filter entfernt - zeige alle Features")
|
||||||
|
} else {
|
||||||
|
// Erstelle WHERE-Klausel für SQL
|
||||||
|
// Beispiel: "Typ IN ('Straße', 'Gehweg')"
|
||||||
|
val typeList = selectedTypes.joinToString("', '", "'", "'")
|
||||||
|
val whereClause = "Typ IN ($typeList)"
|
||||||
|
|
||||||
|
featureLayer.definitionExpression = whereClause
|
||||||
|
println("DEBUG: Filter angewendet: $whereClause")
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
snackBarMessage = if (selectedTypes.isEmpty()) {
|
||||||
|
"Alle Schäden werden angezeigt"
|
||||||
|
} else {
|
||||||
|
"${selectedTypes.size} Typ(en) gefiltert"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("DEBUG: Fehler beim Anwenden des Filters: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
snackBarMessage = "Fehler beim Filtern: ${e.message}"
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension-Funktion für MapViewModel
|
||||||
|
* Gibt die aktuell aktiven Filter zurück
|
||||||
|
*/
|
||||||
|
fun MapViewModel.getActiveFilters(): Set<String> {
|
||||||
|
|
||||||
|
val expression = featureLayer.definitionExpression
|
||||||
|
if (expression.isEmpty()) return emptySet()
|
||||||
|
|
||||||
|
|
||||||
|
val regex = "'([^']+)'".toRegex()
|
||||||
|
return regex.findAll(expression)
|
||||||
|
.map { it.groupValues[1] }
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
|
||||||
|
package com.example.snapandsolve
|
||||||
|
|
||||||
|
import MapViewModel
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.arcgismaps.data.ArcGISFeature
|
||||||
|
import com.arcgismaps.data.Attachment
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable Dialog für Feature-Details mit Bewertung
|
||||||
|
* Zeigt alle Attribute, Fotos und Bewertungsbuttons
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun FeatureInfoDialog(
|
||||||
|
feature: ArcGISFeature?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onRate: (ArcGISFeature, Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
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 isLoadingPhotos by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
// Lade Fotos beim Öffnen
|
||||||
|
LaunchedEffect(feature) {
|
||||||
|
try {
|
||||||
|
// Lade Feature falls nötig
|
||||||
|
if (feature.loadStatus.value != com.arcgismaps.LoadStatus.Loaded) {
|
||||||
|
feature.load().getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole Attachments
|
||||||
|
feature.fetchAttachments().onSuccess { fetchedAttachments ->
|
||||||
|
attachments = fetchedAttachments
|
||||||
|
|
||||||
|
// Lade Foto-Daten
|
||||||
|
val bitmaps = mutableListOf<androidx.compose.ui.graphics.ImageBitmap>()
|
||||||
|
fetchedAttachments.forEach { attachment ->
|
||||||
|
attachment.fetchData().onSuccess { data ->
|
||||||
|
try {
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
|
||||||
|
if (bitmap != null) {
|
||||||
|
bitmaps.add(bitmap.asImageBitmap())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("DEBUG: Fehler beim Laden des Bildes: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
photoBitmaps = bitmaps
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("DEBUG: Fehler beim Laden der Attachments: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
isLoadingPhotos = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.85f),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(20.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
// Header mit Typ
|
||||||
|
Text(
|
||||||
|
text = feature.attributes["Typ"]?.toString() ?: "Straßenschaden",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Divider(modifier = Modifier.padding(bottom = 16.dp))
|
||||||
|
|
||||||
|
// Beschreibung
|
||||||
|
val description = feature.attributes["Beschreibung"]?.toString()
|
||||||
|
if (!description.isNullOrEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "Beschreibung",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fotos
|
||||||
|
if (isLoadingPhotos) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Lade Fotos...")
|
||||||
|
}
|
||||||
|
} else if (photoBitmaps.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "Fotos (${photoBitmaps.size})",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
photoBitmaps.forEach { bitmap ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 8.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
bitmap = bitmap,
|
||||||
|
contentDescription = "Schadensfoto",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 300.dp),
|
||||||
|
contentScale = ContentScale.Fit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Community-Bewertung
|
||||||
|
val currentRating = (feature.attributes["communitycounter"] as? Number)?.toInt() ?: 0
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Community-Bewertung",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "👥",
|
||||||
|
style = MaterialTheme.typography.headlineMedium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "$currentRating",
|
||||||
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = if (currentRating == 1) "Bestätigung" else "Bestätigungen",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info-Text
|
||||||
|
Text(
|
||||||
|
text = "Hast du diesen Schaden auch gesehen?",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bewertungs-Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// Daumen runter
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
onRate(feature, false)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text("👎", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
Text("Nein", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daumen hoch
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onRate(feature, true)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text("👍", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
Text("Ja", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schließen Button
|
||||||
|
TextButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
) {
|
||||||
|
Text("Schließen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension-Funktion für MapViewModel
|
||||||
|
* Aktualisiert die Community-Bewertung eines Features
|
||||||
|
*/
|
||||||
|
suspend fun MapViewModel.updateFeatureRating(
|
||||||
|
feature: ArcGISFeature,
|
||||||
|
isPositive: Boolean,
|
||||||
|
context: Context
|
||||||
|
): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
// Hole aktuelle Bewertung
|
||||||
|
val currentRating = (feature.attributes["communitycounter"] as? Number)?.toInt() ?: 0
|
||||||
|
|
||||||
|
// 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})"
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
docs/AlbumAndroidViewModel.md
Normal file
0
docs/AlbumAndroidViewModel.md
Normal file
0
docs/AlbumEvents.md
Normal file
0
docs/AlbumEvents.md
Normal file
0
docs/AlbumViewModel.md
Normal file
0
docs/AlbumViewModel.md
Normal file
0
docs/AlbumViewState.md
Normal file
0
docs/AlbumViewState.md
Normal file
0
docs/Color.md
Normal file
0
docs/Color.md
Normal file
0
docs/DamageFilterSystem.md
Normal file
0
docs/DamageFilterSystem.md
Normal file
0
docs/FeatureRatingSystem.md
Normal file
0
docs/FeatureRatingSystem.md
Normal file
0
docs/MainActivity.md
Normal file
0
docs/MainActivity.md
Normal file
0
docs/MainScreen.md
Normal file
0
docs/MainScreen.md
Normal file
0
docs/MapSegment.md
Normal file
0
docs/MapSegment.md
Normal file
0
docs/MapViewModel.md
Normal file
0
docs/MapViewModel.md
Normal file
0
docs/SideSlider.md
Normal file
0
docs/SideSlider.md
Normal file
0
docs/Theme.md
Normal file
0
docs/Theme.md
Normal file
0
docs/Type.md
Normal file
0
docs/Type.md
Normal file
0
docs/Widget.md
Normal file
0
docs/Widget.md
Normal file
0
docs/locationHelper.md
Normal file
0
docs/locationHelper.md
Normal file
Reference in New Issue
Block a user