status hinzugefügt
regelbasiertes styling architektur überarbeitet
This commit is contained in:
@@ -59,6 +59,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
|
implementation(libs.androidx.compose.runtime)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -3,54 +3,26 @@ package com.example.snapandsolve
|
|||||||
import DamageFilterDialog
|
import DamageFilterDialog
|
||||||
import DamageListDialog
|
import DamageListDialog
|
||||||
import MapViewModel
|
import MapViewModel
|
||||||
import android.Manifest
|
|
||||||
import android.R.attr.enabled
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.PickVisualMediaRequest
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.AddLocation
|
|
||||||
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
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import applyDamageFilter
|
import applyDamageFilter
|
||||||
import com.arcgismaps.data.ArcGISFeature
|
|
||||||
// Hier holen wir die ArcGIS Klassen
|
|
||||||
import com.arcgismaps.mapping.ArcGISMap
|
|
||||||
import com.arcgismaps.mapping.BasemapStyle
|
|
||||||
import com.arcgismaps.mapping.Viewpoint
|
|
||||||
import com.arcgismaps.toolkit.geoviewcompose.MapView
|
|
||||||
// Hier deine eigenen Klassen (Pfade prüfen!)
|
|
||||||
import com.example.snapandsolve.camera.AlbumViewModel
|
import com.example.snapandsolve.camera.AlbumViewModel
|
||||||
import com.example.snapandsolve.camera.AlbumViewState
|
|
||||||
import com.example.snapandsolve.camera.Intent
|
|
||||||
import com.example.snapandsolve.ui.theme.*
|
import com.example.snapandsolve.ui.theme.*
|
||||||
|
import com.example.snapandsolve.ui.theme.composable.SideSlider
|
||||||
|
import com.example.snapandsolve.ui.theme.composable.SliderMenuItem
|
||||||
|
import com.example.snapandsolve.view.ReportDialog
|
||||||
import getActiveFilters
|
import getActiveFilters
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -60,17 +32,14 @@ import kotlinx.coroutines.launch
|
|||||||
fun MainScreen(modifier: Modifier = Modifier, application: Application) {
|
fun MainScreen(modifier: Modifier = Modifier, application: Application) {
|
||||||
var showReport by rememberSaveable { mutableStateOf(false) }
|
var showReport by rememberSaveable { mutableStateOf(false) }
|
||||||
var sliderOpen by rememberSaveable { mutableStateOf(false) }
|
var sliderOpen by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val mapViewModel = remember { MapViewModel(application) }
|
val mapViewModel = remember { MapViewModel(application) }
|
||||||
val albumViewModel = remember { AlbumViewModel(Dispatchers.Default) }
|
val albumViewModel = remember { AlbumViewModel(Dispatchers.Default) }
|
||||||
|
|
||||||
// test
|
|
||||||
fun openReport() {
|
fun openReport() {
|
||||||
showReport = true
|
showReport = true
|
||||||
sliderOpen = false
|
sliderOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// test
|
|
||||||
fun closeReport() {
|
fun closeReport() {
|
||||||
showReport = false
|
showReport = false
|
||||||
}
|
}
|
||||||
@@ -160,7 +129,7 @@ fun ContentScreen(
|
|||||||
|
|
||||||
// Report Overlay
|
// Report Overlay
|
||||||
if (showReport) {
|
if (showReport) {
|
||||||
ReportOverlay(
|
ReportDialog(
|
||||||
onCancel = onDismissReport,
|
onCancel = onDismissReport,
|
||||||
onClose = onDismissReport,
|
onClose = onDismissReport,
|
||||||
viewModel = albumViewModel,
|
viewModel = albumViewModel,
|
||||||
@@ -236,219 +205,7 @@ fun AppTopBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
/*
|
||||||
fun ReportOverlay(
|
|
||||||
onCancel: () -> Unit,
|
|
||||||
onClose: () -> Unit,
|
|
||||||
viewModel: AlbumViewModel,
|
|
||||||
mapViewModel: MapViewModel
|
|
||||||
) {
|
|
||||||
val viewState: AlbumViewState by viewModel.viewStateFlow.collectAsState()
|
|
||||||
val currentContext = LocalContext.current
|
|
||||||
val hasPoint = mapViewModel.reportDraft.point != null
|
|
||||||
var dropdownExpanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(viewState.selectedPictures) {
|
|
||||||
mapViewModel.updateReportDraft { copy(photos = viewState.selectedPictures) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val pickImageFromAlbumLauncher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.PickMultipleVisualMedia(20)
|
|
||||||
) { urls ->
|
|
||||||
viewModel.onReceive(Intent.OnFinishPickingImagesWith(currentContext, urls))
|
|
||||||
}
|
|
||||||
|
|
||||||
val cameraLauncher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.TakePicture()
|
|
||||||
) { isImageSaved ->
|
|
||||||
if (isImageSaved) viewModel.onReceive(Intent.OnImageSavedWith(currentContext))
|
|
||||||
else viewModel.onReceive(Intent.OnImageSavingCanceled)
|
|
||||||
}
|
|
||||||
|
|
||||||
val permissionLauncher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { permissionGranted ->
|
|
||||||
if (permissionGranted) viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
|
|
||||||
else viewModel.onReceive(Intent.OnPermissionDenied)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startCamera() {
|
|
||||||
val hasPermission = currentContext.checkSelfPermission(Manifest.permission.CAMERA) ==
|
|
||||||
android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
||||||
if (hasPermission) viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
|
|
||||||
else permissionLauncher.launch(Manifest.permission.CAMERA)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(viewState.tempFileUrl) {
|
|
||||||
viewState.tempFileUrl?.let { cameraLauncher.launch(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayShell(
|
|
||||||
title = "Neue Meldung",
|
|
||||||
footer = {
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = {
|
|
||||||
mapViewModel.resetDraft()
|
|
||||||
viewModel.clearSelection()
|
|
||||||
onCancel()
|
|
||||||
}
|
|
||||||
) { Text("Abbrechen", color = Color.Black) }
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
mapViewModel.submitDraftToLayer()
|
|
||||||
viewModel.clearSelection()
|
|
||||||
onCancel()
|
|
||||||
},
|
|
||||||
enabled = mapViewModel.reportDraft.isValid
|
|
||||||
) { Text("Hinzufügen") }
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text("Schadensbeschreibung:", color = Color.Black)
|
|
||||||
|
|
||||||
// Typ Dropdown
|
|
||||||
Box {
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = { dropdownExpanded = true },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text("Typ: ${mapViewModel.reportDraft.typ}")
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = dropdownExpanded,
|
|
||||||
onDismissRequest = { dropdownExpanded = false }
|
|
||||||
) {
|
|
||||||
MapViewModel.DAMAGE_TYPES.forEach { typ -> // <-- Nutzt zentrale Liste
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(typ) },
|
|
||||||
onClick = {
|
|
||||||
mapViewModel.updateReportDraft { copy(typ = typ) }
|
|
||||||
dropdownExpanded = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kamera Buttons
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
Button(onClick = { startCamera() }, modifier = Modifier.weight(1f)) {
|
|
||||||
Text("Foto aufnehmen")
|
|
||||||
}
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
pickImageFromAlbumLauncher.launch(
|
|
||||||
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Text("Aus Galerie")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Beschreibung
|
|
||||||
TextField(
|
|
||||||
value = mapViewModel.reportDraft.beschreibung,
|
|
||||||
onValueChange = { text -> mapViewModel.updateReportDraft { copy(beschreibung = text) } },
|
|
||||||
modifier = Modifier.fillMaxWidth().height(220.dp),
|
|
||||||
placeholder = { Text("Beschreibung eingeben...") },
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = Color.White,
|
|
||||||
unfocusedContainerColor = Color.White
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Bilder Grid (body ist scrollbar, grid selbst nicht)
|
|
||||||
if (viewState.selectedPictures.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = "Ausgewählte Bilder (${viewState.selectedPictures.size})",
|
|
||||||
color = Color.Black
|
|
||||||
)
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = GridCells.Fixed(3),
|
|
||||||
userScrollEnabled = false,
|
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(0.dp, 200.dp)
|
|
||||||
) {
|
|
||||||
itemsIndexed(viewState.selectedPictures) { index, picture ->
|
|
||||||
Image(
|
|
||||||
modifier = Modifier.padding(4.dp),
|
|
||||||
bitmap = picture,
|
|
||||||
contentDescription = "Bild ${index + 1}",
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position Buttons
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
mapViewModel.pickCurrentLocation()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(if (hasPoint) "Neue Position aus Standort" else "Position aus Standort")
|
|
||||||
}
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
mapViewModel.startPickReportLocation()
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(if (hasPoint) "Position neu setzen" else "Position manuell setzen")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun FeatureInfoOverlay(
|
|
||||||
feature: ArcGISFeature,
|
|
||||||
onClose: () -> Unit
|
|
||||||
) {
|
|
||||||
val typ = feature.attributes["Typ"].toString()
|
|
||||||
val beschreibung = feature.attributes["Beschreibung"].toString()
|
|
||||||
val id = feature.attributes["OBJECTID"].toString()
|
|
||||||
var image by remember { mutableStateOf<ImageBitmap?>(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(feature) {
|
|
||||||
image = loadFirstAttachmentBitmap(feature)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
OverlayShell(
|
|
||||||
title = "Meldung $id",
|
|
||||||
footer = {
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = {
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
) { Text("Schließen", color = Color.Black) }
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
image?.let {
|
|
||||||
Image(
|
|
||||||
bitmap = it,
|
|
||||||
contentDescription = "Feature Bild",
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(200.dp),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text("Typ: $typ", color = Color.Black)
|
|
||||||
Text("Beschreibung:", color = Color.Black)
|
|
||||||
Text(beschreibung, color = Color.Black)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun loadFirstAttachmentBitmap(
|
suspend fun loadFirstAttachmentBitmap(
|
||||||
feature: ArcGISFeature
|
feature: ArcGISFeature
|
||||||
): ImageBitmap? {
|
): ImageBitmap? {
|
||||||
@@ -467,4 +224,5 @@ suspend fun loadFirstAttachmentBitmap(
|
|||||||
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
|
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
|
||||||
return bitmap.asImageBitmap()
|
return bitmap.asImageBitmap()
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,13 @@ import com.arcgismaps.mapping.view.LocationDisplay
|
|||||||
import com.arcgismaps.mapping.view.ScreenCoordinate
|
import com.arcgismaps.mapping.view.ScreenCoordinate
|
||||||
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
|
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
|
||||||
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
|
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
|
||||||
|
import com.example.snapandsolve.view.createTypStatusRenderer
|
||||||
|
import com.example.snapandsolve.viewmodel.findNearbyDamageOfSameType
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MapViewModel(application: Application) : AndroidViewModel(application) {
|
class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -47,6 +50,8 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
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 duplicateDamages by mutableStateOf<List<DamageWithDistance>>(emptyList())
|
||||||
|
private set
|
||||||
var selectedOperation by mutableStateOf(FeatureOperationType.DEFAULT)
|
var selectedOperation by mutableStateOf(FeatureOperationType.DEFAULT)
|
||||||
var reopenReport by mutableStateOf(false)
|
var reopenReport by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
@@ -74,7 +79,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/si_StrassenSchaeden/FeatureServer/0")
|
serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/ArcGIS/rest/services/251120_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
|
||||||
@@ -94,6 +99,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
println("DEBUG: Fehler beim Laden der Tabelle: ${it.message}")
|
println("DEBUG: Fehler beim Laden der Tabelle: ${it.message}")
|
||||||
}
|
}
|
||||||
featureLayer = FeatureLayer.createWithFeatureTable(serviceFeatureTable)
|
featureLayer = FeatureLayer.createWithFeatureTable(serviceFeatureTable)
|
||||||
|
featureLayer.renderer = createTypStatusRenderer()
|
||||||
map.operationalLayers.add(featureLayer)
|
map.operationalLayers.add(featureLayer)
|
||||||
|
|
||||||
// ===== DEBUG: Felder nach dem Hinzufügen zur Map =====
|
// ===== DEBUG: Felder nach dem Hinzufügen zur Map =====
|
||||||
@@ -359,6 +365,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
geometry = draft.point
|
geometry = draft.point
|
||||||
attributes["Beschreibung"] = draft.beschreibung
|
attributes["Beschreibung"] = draft.beschreibung
|
||||||
attributes["Typ"] = draft.typ
|
attributes["Typ"] = draft.typ
|
||||||
|
attributes["status"] = draft.status
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Erst addFeature + applyEdits => Feature existiert am Server (ObjectID)
|
// 2) Erst addFeature + applyEdits => Feature existiert am Server (ObjectID)
|
||||||
@@ -405,6 +412,17 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
selectedFeature = null
|
selectedFeature = null
|
||||||
featureLayer.clearSelection()
|
featureLayer.clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun isDuplicateNearby(radiusMeters: Double): Boolean {
|
||||||
|
val p = reportDraft.point ?: return false
|
||||||
|
val t = reportDraft.typ
|
||||||
|
duplicateDamages = findNearbyDamageOfSameType(serviceFeatureTable, p, t, radiusMeters)
|
||||||
|
return duplicateDamages.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearDuplicateDamages() {
|
||||||
|
duplicateDamages = emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class FeatureOperationType(val operationName: String, val instruction: String) {
|
enum class FeatureOperationType(val operationName: String, val instruction: String) {
|
||||||
@@ -419,7 +437,8 @@ data class ReportDraft(
|
|||||||
val beschreibung: String = "",
|
val beschreibung: String = "",
|
||||||
val typ: String = "Schadenstyp wählen...",
|
val typ: String = "Schadenstyp wählen...",
|
||||||
val photos: List<ImageBitmap> = emptyList(),
|
val photos: List<ImageBitmap> = emptyList(),
|
||||||
val point: Point? = null
|
val point: Point? = null,
|
||||||
|
val status: String = "neu"
|
||||||
) {
|
) {
|
||||||
val isValid: Boolean
|
val isValid: Boolean
|
||||||
get() =
|
get() =
|
||||||
|
|||||||
@@ -17,6 +17,49 @@ import kotlinx.coroutines.withContext
|
|||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog für Schaden-Filter
|
* Dialog für Schaden-Filter
|
||||||
* Ermöglicht das Filtern von Features nach Typ UND Datum (unabhängig voneinander)
|
* Ermöglicht das Filtern von Features nach Typ UND Datum (unabhängig voneinander)
|
||||||
@@ -268,48 +311,6 @@ fun DamageFilterDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* Extension-Funktion für MapViewModel
|
||||||
* Wendet Filter auf den FeatureLayer an (Typ + Datum - UNABHÄNGIG)
|
* Wendet Filter auf den FeatureLayer an (Typ + Datum - UNABHÄNGIG)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import MapViewModel
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -11,8 +10,6 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Image
|
|
||||||
import androidx.compose.material.icons.filled.LocationOn
|
|
||||||
import androidx.compose.material.icons.filled.MyLocation
|
import androidx.compose.material.icons.filled.MyLocation
|
||||||
import androidx.compose.material.icons.filled.TrendingUp
|
import androidx.compose.material.icons.filled.TrendingUp
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
@@ -20,7 +17,6 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
@@ -44,7 +40,7 @@ import kotlin.math.sqrt
|
|||||||
*/
|
*/
|
||||||
data class DamageWithDistance(
|
data class DamageWithDistance(
|
||||||
val feature: ArcGISFeature,
|
val feature: ArcGISFeature,
|
||||||
val distanceInMeters: Double,
|
val distanceInMeters: Double?,
|
||||||
val typ: String,
|
val typ: String,
|
||||||
val beschreibung: String,
|
val beschreibung: String,
|
||||||
val objectId: Long,
|
val objectId: Long,
|
||||||
@@ -639,9 +635,9 @@ suspend fun MapViewModel.loadDamagesNearby(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatDistance(meters: Double): String {
|
fun formatDistance(meters: Double?): String {
|
||||||
return if (meters < 1000) {
|
return if (meters == null || meters < 1000) {
|
||||||
"${meters.roundToInt()} m"
|
"${meters?.roundToInt()} m"
|
||||||
} else {
|
} else {
|
||||||
"${"%.1f".format(meters / 1000)} km"
|
"${"%.1f".format(meters / 1000)} km"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,339 @@
|
|||||||
|
package com.example.snapandsolve.ui.theme.composable
|
||||||
|
|
||||||
|
import DamageWithDistance
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import formatDistance
|
||||||
|
import getEmojiForType
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogContainer(
|
||||||
|
title: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentPadding: PaddingValues = PaddingValues(20.dp),
|
||||||
|
maxWidthFraction: Float = 0.9f,
|
||||||
|
maxHeightFraction: Float = 0.85f,
|
||||||
|
footer: (@Composable () -> Unit)? = null,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth(maxWidthFraction)
|
||||||
|
.fillMaxHeight(maxHeightFraction),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(contentPadding)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// ---------- HEADER ----------
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "Schließen"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
|
||||||
|
// ---------- CONTENT (scrollable) ----------
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- FOOTER ----------
|
||||||
|
if (footer != null) {
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
footer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EqualWidthButtonRow(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String? = null,
|
||||||
|
spacing: Dp = 12.dp,
|
||||||
|
content: @Composable RowScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
|
|
||||||
|
if (title != null) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(IntrinsicSize.Min),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class AppButtonStyle { Filled, Outlined }
|
||||||
|
|
||||||
|
data class AppButtonColors(
|
||||||
|
val container: Color? = null,
|
||||||
|
val content: Color? = null,
|
||||||
|
val border: Color? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppButton(
|
||||||
|
text: () -> String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
style: AppButtonStyle = AppButtonStyle.Filled,
|
||||||
|
icon: ImageVector? = null,
|
||||||
|
contentDescription: String? = null,
|
||||||
|
colors: AppButtonColors? = null
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
// 👇 Explizite, gute Defaults
|
||||||
|
val defaultContainer = scheme.primary // klassisches Blau
|
||||||
|
val defaultContent = scheme.onPrimary // weiß
|
||||||
|
val disabledContainer = scheme.surfaceVariant
|
||||||
|
val disabledContent = scheme.onSurfaceVariant
|
||||||
|
|
||||||
|
val content: @Composable RowScope.() -> Unit = {
|
||||||
|
if (icon != null) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(2.dp))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = text(),
|
||||||
|
maxLines = Int.MAX_VALUE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (style) {
|
||||||
|
AppButtonStyle.Filled -> Button(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
modifier = modifier,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = colors?.container ?: defaultContainer,
|
||||||
|
contentColor = colors?.content ?: defaultContent,
|
||||||
|
disabledContainerColor = disabledContainer,
|
||||||
|
disabledContentColor = disabledContent
|
||||||
|
),
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
|
||||||
|
AppButtonStyle.Outlined -> OutlinedButton(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
modifier = modifier,
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = colors?.content ?: scheme.primary,
|
||||||
|
disabledContentColor = disabledContent
|
||||||
|
),
|
||||||
|
border = BorderStroke(
|
||||||
|
1.dp,
|
||||||
|
colors?.border ?: scheme.primary
|
||||||
|
),
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DamageListItem(
|
||||||
|
damage: DamageWithDistance,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(100.dp)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Links: Foto
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(100.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.clip(RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp))
|
||||||
|
) {
|
||||||
|
if (damage.photo != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = damage.photo,
|
||||||
|
contentDescription = "Schadensfoto",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text("📷", style = MaterialTheme.typography.headlineLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rechts: Infos
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f)
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(getEmojiForType(damage.typ), style = MaterialTheme.typography.titleLarge)
|
||||||
|
Text(
|
||||||
|
text = damage.typ,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shortDesc =
|
||||||
|
damage.beschreibung.take(40) + if (damage.beschreibung.length > 40) "..." else ""
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = shortDesc,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text("📍", style = MaterialTheme.typography.labelSmall)
|
||||||
|
Text(
|
||||||
|
text = formatDistance(damage.distanceInMeters),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text("👥", style = MaterialTheme.typography.labelSmall)
|
||||||
|
Text(
|
||||||
|
text = "${damage.rating}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = if (damage.rating > 0)
|
||||||
|
MaterialTheme.colorScheme.tertiary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.example.snapandsolve.ui.theme.composable
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LegendItem(
|
||||||
|
label: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
marker: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
marker()
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LegendMarkerCircle(color: Color) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(12.dp)
|
||||||
|
.background(color, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LegendMarkerIcon(
|
||||||
|
@DrawableRes iconRes: Int,
|
||||||
|
tint: Color? = null
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(iconRes),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = tint ?: Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.snapandsolve.ui.theme
|
package com.example.snapandsolve.ui.theme.composable
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -15,6 +15,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import com.example.snapandsolve.ui.theme.WidgetColor
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.example.snapandsolve.view
|
||||||
|
|
||||||
|
import DamageWithDistance
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.example.snapandsolve.ui.theme.composable.AppButton
|
||||||
|
import com.example.snapandsolve.ui.theme.composable.AppButtonStyle
|
||||||
|
import com.example.snapandsolve.ui.theme.composable.DamageListItem
|
||||||
|
import com.example.snapandsolve.ui.theme.composable.DialogContainer
|
||||||
|
import com.example.snapandsolve.ui.theme.composable.EqualWidthButtonRow
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CloseDamageDialog(
|
||||||
|
hits: List<DamageWithDistance>,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onProceedAnyway: () -> Unit,
|
||||||
|
) {
|
||||||
|
DialogContainer(
|
||||||
|
title = "Ähnliche Schäden in der Nähe",
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
footer = {
|
||||||
|
EqualWidthButtonRow {
|
||||||
|
AppButton(
|
||||||
|
text = { "Abbrechen" },
|
||||||
|
style = AppButtonStyle.Outlined,
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
AppButton(
|
||||||
|
text = { "Trotzdem hinzufügen" },
|
||||||
|
style = AppButtonStyle.Filled,
|
||||||
|
onClick = onProceedAnyway,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Es wurden ${hits.size} ähnliche Meldung(en) in deiner Nähe.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Liste
|
||||||
|
hits.forEach { hit ->
|
||||||
|
DamageListItem(
|
||||||
|
damage = hit,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
253
app/src/main/java/com/example/snapandsolve/view/ReportDialog.kt
Normal file
253
app/src/main/java/com/example/snapandsolve/view/ReportDialog.kt
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
package com.example.snapandsolve.view
|
||||||
|
|
||||||
|
import MapViewModel
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.example.snapandsolve.camera.AlbumViewModel
|
||||||
|
import com.example.snapandsolve.camera.AlbumViewState
|
||||||
|
import com.example.snapandsolve.camera.Intent
|
||||||
|
import com.example.snapandsolve.ui.theme.composable.AppButton
|
||||||
|
import com.example.snapandsolve.ui.theme.composable.AppButtonStyle
|
||||||
|
import com.example.snapandsolve.ui.theme.composable.DialogContainer
|
||||||
|
import com.example.snapandsolve.ui.theme.composable.EqualWidthButtonRow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReportDialog(
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
viewModel: AlbumViewModel,
|
||||||
|
mapViewModel: MapViewModel
|
||||||
|
) {
|
||||||
|
val viewState: AlbumViewState by viewModel.viewStateFlow.collectAsState()
|
||||||
|
val currentContext = LocalContext.current
|
||||||
|
val hasPoint = mapViewModel.reportDraft.point != null
|
||||||
|
var dropdownExpanded by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var showDuplicateDialog by remember { mutableStateOf(false) }
|
||||||
|
var checking by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(viewState.selectedPictures) {
|
||||||
|
mapViewModel.updateReportDraft { copy(photos = viewState.selectedPictures) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val pickImageFromAlbumLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.PickMultipleVisualMedia(20)
|
||||||
|
) { urls ->
|
||||||
|
viewModel.onReceive(Intent.OnFinishPickingImagesWith(currentContext, urls))
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.TakePicture()
|
||||||
|
) { isImageSaved ->
|
||||||
|
if (isImageSaved) viewModel.onReceive(Intent.OnImageSavedWith(currentContext))
|
||||||
|
else viewModel.onReceive(Intent.OnImageSavingCanceled)
|
||||||
|
}
|
||||||
|
|
||||||
|
val permissionLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { permissionGranted ->
|
||||||
|
if (permissionGranted) viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
|
||||||
|
else viewModel.onReceive(Intent.OnPermissionDenied)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startCamera() {
|
||||||
|
val hasPermission =
|
||||||
|
currentContext.checkSelfPermission(Manifest.permission.CAMERA) ==
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
if (hasPermission) viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
|
||||||
|
else permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(viewState.tempFileUrl) {
|
||||||
|
viewState.tempFileUrl?.let { cameraLauncher.launch(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogContainer(
|
||||||
|
title = "Neue Meldung",
|
||||||
|
onDismiss = onCancel,
|
||||||
|
maxWidthFraction = 0.98f,
|
||||||
|
maxHeightFraction = 0.95f,
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
footer = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
AppButton(
|
||||||
|
text = { "Hinzufügen" },
|
||||||
|
style = AppButtonStyle.Outlined,
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
checking = true
|
||||||
|
showDuplicateDialog = mapViewModel.isDuplicateNearby(20.0)
|
||||||
|
checking = false
|
||||||
|
|
||||||
|
if (!showDuplicateDialog) {
|
||||||
|
mapViewModel.submitDraftToLayer()
|
||||||
|
viewModel.clearSelection()
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = mapViewModel.reportDraft.isValid,
|
||||||
|
modifier = Modifier.fillMaxWidth(0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// ---------- CONTENT ----------
|
||||||
|
Text(
|
||||||
|
text = "Schadensbeschreibung:",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Typ Dropdown
|
||||||
|
Box {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { dropdownExpanded = true },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Typ: ${mapViewModel.reportDraft.typ}")
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = dropdownExpanded,
|
||||||
|
onDismissRequest = { dropdownExpanded = false }
|
||||||
|
) {
|
||||||
|
MapViewModel.DAMAGE_TYPES.forEach { typ ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(typ) },
|
||||||
|
onClick = {
|
||||||
|
mapViewModel.updateReportDraft { copy(typ = typ) }
|
||||||
|
dropdownExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Kamera Buttons
|
||||||
|
EqualWidthButtonRow(title = "Foto aufnehmen...") {
|
||||||
|
AppButton(
|
||||||
|
text = { "Foto aufnehmen" },
|
||||||
|
style = AppButtonStyle.Filled,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { startCamera() }
|
||||||
|
)
|
||||||
|
AppButton(
|
||||||
|
text = { "Aus Galerie" },
|
||||||
|
style = AppButtonStyle.Filled,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = {
|
||||||
|
pickImageFromAlbumLauncher.launch(
|
||||||
|
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Beschreibung
|
||||||
|
TextField(
|
||||||
|
value = mapViewModel.reportDraft.beschreibung,
|
||||||
|
onValueChange = { text ->
|
||||||
|
mapViewModel.updateReportDraft { copy(beschreibung = text) }
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(220.dp),
|
||||||
|
placeholder = { Text("Beschreibung eingeben...") },
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = Color.White,
|
||||||
|
unfocusedContainerColor = Color.White
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Bilder Grid
|
||||||
|
if (viewState.selectedPictures.isNotEmpty()) {
|
||||||
|
Text("Ausgewählte Bilder (${viewState.selectedPictures.size})")
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(3),
|
||||||
|
userScrollEnabled = false,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(0.dp, 200.dp)
|
||||||
|
) {
|
||||||
|
itemsIndexed(viewState.selectedPictures) { index, picture ->
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.padding(4.dp),
|
||||||
|
bitmap = picture,
|
||||||
|
contentDescription = "Bild ${index + 1}",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Position Buttons
|
||||||
|
EqualWidthButtonRow(title = "Position...") {
|
||||||
|
AppButton(
|
||||||
|
text = { if (hasPoint) "neu aus Standort" else "aus Standort" },
|
||||||
|
style = AppButtonStyle.Filled,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { mapViewModel.pickCurrentLocation() }
|
||||||
|
)
|
||||||
|
AppButton(
|
||||||
|
text = { if (hasPoint) "manuell neu setzen" else "manuell setzen" },
|
||||||
|
style = AppButtonStyle.Filled,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = {
|
||||||
|
mapViewModel.startPickReportLocation()
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showDuplicateDialog) {
|
||||||
|
CloseDamageDialog(
|
||||||
|
hits = mapViewModel.duplicateDamages,
|
||||||
|
onDismiss = {
|
||||||
|
showDuplicateDialog = false
|
||||||
|
mapViewModel.clearDuplicateDamages()
|
||||||
|
},
|
||||||
|
onProceedAnyway = {
|
||||||
|
mapViewModel.submitDraftToLayer()
|
||||||
|
viewModel.clearSelection()
|
||||||
|
showDuplicateDialog = false
|
||||||
|
mapViewModel.clearDuplicateDamages()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.example.snapandsolve.view
|
||||||
|
|
||||||
|
import com.arcgismaps.Color
|
||||||
|
import com.arcgismaps.mapping.symbology.*
|
||||||
|
import kotlin.collections.iterator
|
||||||
|
|
||||||
|
fun createTypStatusRenderer(): UniqueValueRenderer {
|
||||||
|
|
||||||
|
// Status -> Hintergrundfarbe
|
||||||
|
val statusColor = mapOf(
|
||||||
|
"neu" to Color.fromRgba(220, 50, 50, 255),
|
||||||
|
"in Bearbeitung" to Color.fromRgba(255, 180, 0, 255),
|
||||||
|
"Schaden behoben" to Color.fromRgba(60, 180, 75, 255)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Typ -> Icon-Style (aus Standardbibliothek)
|
||||||
|
val typeIcon = mapOf(
|
||||||
|
"Straße" to SimpleMarkerSymbolStyle.Square,
|
||||||
|
"Gehweg" to SimpleMarkerSymbolStyle.Triangle,
|
||||||
|
"Fahrradweg" to SimpleMarkerSymbolStyle.Diamond,
|
||||||
|
"Beleuchtung" to SimpleMarkerSymbolStyle.X,
|
||||||
|
"Sonstiges" to SimpleMarkerSymbolStyle.Cross
|
||||||
|
)
|
||||||
|
|
||||||
|
val renderer = UniqueValueRenderer().apply {
|
||||||
|
fieldNames.addAll(listOf("typ", "status"))
|
||||||
|
|
||||||
|
defaultLabel = "Sonstige"
|
||||||
|
defaultSymbol = makeStatusTypeSymbol(
|
||||||
|
iconStyle = SimpleMarkerSymbolStyle.Circle,
|
||||||
|
backgroundFill = Color.fromRgba(180, 180, 180, 255)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((typ, iconStyle) in typeIcon) {
|
||||||
|
for ((status, bgColor) in statusColor) {
|
||||||
|
val label = "$typ • $status"
|
||||||
|
renderer.uniqueValues.add(
|
||||||
|
UniqueValue(
|
||||||
|
label = label,
|
||||||
|
description = label,
|
||||||
|
symbol = makeStatusTypeSymbol(iconStyle, bgColor),
|
||||||
|
values = listOf(typ, status)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderer
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeStatusTypeSymbol(
|
||||||
|
iconStyle: SimpleMarkerSymbolStyle,
|
||||||
|
backgroundFill: Color
|
||||||
|
): Symbol {
|
||||||
|
val white = Color.fromRgba(255, 255, 255, 255)
|
||||||
|
|
||||||
|
// Hintergrund: Kreis in Statusfarbe
|
||||||
|
val background = SimpleMarkerSymbol(
|
||||||
|
style = SimpleMarkerSymbolStyle.Circle,
|
||||||
|
color = backgroundFill,
|
||||||
|
size = 18f
|
||||||
|
).apply {
|
||||||
|
outline = SimpleLineSymbol(
|
||||||
|
style = SimpleLineSymbolStyle.Solid,
|
||||||
|
color = white,
|
||||||
|
width = 1.5f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vordergrund: "Icon" als Outline (weiß), ohne Füllung
|
||||||
|
val foreground = SimpleMarkerSymbol(
|
||||||
|
style = iconStyle,
|
||||||
|
color = Color.fromRgba(0, 0, 0, 0), // transparent => nur Outline sichtbar
|
||||||
|
size = 10f
|
||||||
|
).apply {
|
||||||
|
outline = SimpleLineSymbol(
|
||||||
|
style = SimpleLineSymbolStyle.Solid,
|
||||||
|
color = white,
|
||||||
|
width = 2.0f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompositeSymbol(listOf(background, foreground))
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.example.snapandsolve.viewmodel
|
||||||
|
|
||||||
|
import DamageWithDistance
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import com.arcgismaps.data.ArcGISFeature
|
||||||
|
import com.arcgismaps.data.QueryParameters
|
||||||
|
import com.arcgismaps.data.ServiceFeatureTable
|
||||||
|
import com.arcgismaps.geometry.GeometryEngine
|
||||||
|
import com.arcgismaps.geometry.Point
|
||||||
|
import com.arcgismaps.geometry.SpatialReference
|
||||||
|
|
||||||
|
suspend fun findNearbyDamageOfSameType(
|
||||||
|
table: ServiceFeatureTable,
|
||||||
|
draftPoint: Point,
|
||||||
|
draftTyp: String,
|
||||||
|
radiusMeters: Double
|
||||||
|
): List<DamageWithDistance> {
|
||||||
|
|
||||||
|
val layerSR: SpatialReference? = table.spatialReference
|
||||||
|
val pointInLayerSR = if (layerSR != null && draftPoint.spatialReference != layerSR) {
|
||||||
|
(GeometryEngine.projectOrNull(draftPoint, layerSR) as? Point) ?: draftPoint
|
||||||
|
} else {
|
||||||
|
draftPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
val buffer = GeometryEngine.bufferOrNull(pointInLayerSR, radiusMeters) ?: return emptyList()
|
||||||
|
|
||||||
|
val safeTyp = draftTyp.replace("'", "''")
|
||||||
|
val qp = QueryParameters().apply {
|
||||||
|
whereClause = "typ = '$safeTyp'"
|
||||||
|
geometry = buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = table.queryFeatures(qp).getOrThrow()
|
||||||
|
|
||||||
|
val hits = mutableListOf<DamageWithDistance>()
|
||||||
|
|
||||||
|
for (feature in result) {
|
||||||
|
val p = feature.geometry as? Point ?: continue
|
||||||
|
|
||||||
|
// ✅ Distanz korrekt
|
||||||
|
val dist = GeometryEngine.distanceOrNull(pointInLayerSR, p) ?: Double.POSITIVE_INFINITY
|
||||||
|
|
||||||
|
val typ = (feature.attributes["typ"] as? String).orEmpty()
|
||||||
|
val beschreibung = (feature.attributes["beschreibung"] as? String).orEmpty()
|
||||||
|
val rating = (feature.attributes["communitycounter"] as? Number)?.toInt() ?: 0
|
||||||
|
val id = (feature.attributes["OBJECTID"] as? Number)?.toLong() ?: 0L
|
||||||
|
val photo: ImageBitmap? = try {
|
||||||
|
val arcFeature = feature as? ArcGISFeature
|
||||||
|
val attachments = arcFeature?.fetchAttachments()?.getOrNull()
|
||||||
|
val first = attachments?.firstOrNull()
|
||||||
|
|
||||||
|
// Je nach SDK: first.fetchData() oder first.data
|
||||||
|
val bytes: ByteArray? = first
|
||||||
|
?.fetchData()
|
||||||
|
?.getOrNull()
|
||||||
|
|
||||||
|
bytes?.let {
|
||||||
|
BitmapFactory.decodeByteArray(it, 0, it.size)?.asImageBitmap()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
hits += DamageWithDistance(
|
||||||
|
feature = feature as ArcGISFeature,
|
||||||
|
typ = typ,
|
||||||
|
beschreibung = beschreibung,
|
||||||
|
photo = photo,
|
||||||
|
rating = rating,
|
||||||
|
distanceInMeters = dist,
|
||||||
|
objectId = id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hits.sortedBy { it.distanceInMeters }
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ activityCompose = "1.12.1"
|
|||||||
composeBom = "2024.09.00"
|
composeBom = "2024.09.00"
|
||||||
material3 = "1.4.0"
|
material3 = "1.4.0"
|
||||||
arcgisMapsKotlin = "200.8.0"
|
arcgisMapsKotlin = "200.8.0"
|
||||||
|
runtime = "1.10.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -31,6 +32,7 @@ arcgis-maps-kotlin = { group = "com.esri", name = "arcgis-maps-kotlin", version.
|
|||||||
arcgis-maps-kotlin-toolkit-bom = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-bom", version.ref = "arcgisMapsKotlin" }
|
arcgis-maps-kotlin-toolkit-bom = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-bom", version.ref = "arcgisMapsKotlin" }
|
||||||
arcgis-maps-kotlin-toolkit-geoview-compose = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-geoview-compose" }
|
arcgis-maps-kotlin-toolkit-geoview-compose = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-geoview-compose" }
|
||||||
arcgis-maps-kotlin-toolkit-authentication = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-authentication" }
|
arcgis-maps-kotlin-toolkit-authentication = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-authentication" }
|
||||||
|
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user