- Filtern nach Status
- App Meldungen über nahe Schäden erhalten.
This commit is contained in:
2
.idea/deploymentTargetSelector.xml
generated
2
.idea/deploymentTargetSelector.xml
generated
@@ -4,7 +4,7 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2026-01-25T17:20:21.835673100Z">
|
<DropdownSelection timestamp="2026-02-06T10:29:08.485527Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\sklau\.android\avd\Medium_Phone.avd" />
|
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\sklau\.android\avd\Medium_Phone.avd" />
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -19,6 +23,7 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.SnapAndSolve">
|
android:theme="@style/Theme.SnapAndSolve">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.example.snapandsolve.MainActivity"
|
android:name="com.example.snapandsolve.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -26,10 +31,17 @@
|
|||||||
android:theme="@style/Theme.SnapAndSolve">
|
android:theme="@style/Theme.SnapAndSolve">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- Service für Proximity-Benachrichtigungen -->
|
||||||
|
<service
|
||||||
|
android:name=".service.ProximityNotificationService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="location" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.snapandsolve
|
package com.example.snapandsolve
|
||||||
|
|
||||||
|
import MainScreen
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
package com.example.snapandsolve
|
|
||||||
|
|
||||||
import DamageFilterDialog
|
import DamageFilterDialog
|
||||||
import DamageListDialog
|
import DamageListDialog
|
||||||
import MapViewModel
|
import MapViewModel
|
||||||
@@ -10,6 +8,8 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
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.Settings
|
||||||
|
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
|
||||||
@@ -18,10 +18,18 @@ import androidx.compose.ui.graphics.Color
|
|||||||
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
|
||||||
|
import com.arcgismaps.mapping.ArcGISMap
|
||||||
|
import com.arcgismaps.mapping.BasemapStyle
|
||||||
|
import com.arcgismaps.mapping.Viewpoint
|
||||||
|
import com.arcgismaps.toolkit.geoviewcompose.MapView
|
||||||
|
import com.example.snapandsolve.FeatureInfoDialog
|
||||||
|
import com.example.snapandsolve.MapSegment
|
||||||
import com.example.snapandsolve.camera.AlbumViewModel
|
import com.example.snapandsolve.camera.AlbumViewModel
|
||||||
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.SideSlider
|
||||||
import com.example.snapandsolve.ui.theme.composable.SliderMenuItem
|
import com.example.snapandsolve.ui.theme.composable.SliderMenuItem
|
||||||
|
import com.example.snapandsolve.updateFeatureRating
|
||||||
import com.example.snapandsolve.view.ReportDialog
|
import com.example.snapandsolve.view.ReportDialog
|
||||||
import getActiveFilters
|
import getActiveFilters
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -32,6 +40,8 @@ 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) }
|
||||||
|
var showSettings 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) }
|
||||||
|
|
||||||
@@ -44,6 +54,15 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
|
|||||||
showReport = false
|
showReport = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openSettings() {
|
||||||
|
showSettings = true
|
||||||
|
sliderOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeSettings() {
|
||||||
|
showSettings = false
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(mapViewModel.reopenReport) {
|
LaunchedEffect(mapViewModel.reopenReport) {
|
||||||
if (mapViewModel.reopenReport) {
|
if (mapViewModel.reopenReport) {
|
||||||
showReport = true
|
showReport = true
|
||||||
@@ -51,7 +70,6 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
topBar = { AppTopBar() },
|
topBar = { AppTopBar() },
|
||||||
@@ -60,7 +78,7 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
|
|||||||
modifier = Modifier.height(120.dp),
|
modifier = Modifier.height(120.dp),
|
||||||
containerColor = AppColor,
|
containerColor = AppColor,
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = { sliderOpen = !sliderOpen; showReport = false }) {
|
IconButton(onClick = { sliderOpen = !sliderOpen; showReport = false; showSettings = false }) {
|
||||||
Icon(Icons.Default.Menu, contentDescription = "Menu")
|
Icon(Icons.Default.Menu, contentDescription = "Menu")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,14 +98,22 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
|
|||||||
},
|
},
|
||||||
floatingActionButtonPosition = FabPosition.Center,
|
floatingActionButtonPosition = FabPosition.Center,
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
ContentScreen(
|
if (showSettings) {
|
||||||
modifier = Modifier.padding(innerPadding),
|
SettingsScreen(
|
||||||
mapViewModel = mapViewModel,
|
onBack = ::closeSettings,
|
||||||
albumViewModel = albumViewModel,
|
mapViewModel = mapViewModel
|
||||||
showReport = showReport,
|
)
|
||||||
sliderOpen = sliderOpen,
|
} else {
|
||||||
onDismissReport = ::closeReport
|
ContentScreen(
|
||||||
)
|
modifier = Modifier.padding(innerPadding),
|
||||||
|
mapViewModel = mapViewModel,
|
||||||
|
albumViewModel = albumViewModel,
|
||||||
|
showReport = showReport,
|
||||||
|
sliderOpen = sliderOpen,
|
||||||
|
onDismissReport = ::closeReport,
|
||||||
|
onOpenSettings = ::openSettings
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,14 +124,13 @@ fun ContentScreen(
|
|||||||
albumViewModel: AlbumViewModel,
|
albumViewModel: AlbumViewModel,
|
||||||
showReport: Boolean,
|
showReport: Boolean,
|
||||||
sliderOpen: Boolean,
|
sliderOpen: Boolean,
|
||||||
onDismissReport: () -> Unit
|
onDismissReport: () -> Unit,
|
||||||
|
onOpenSettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
// NEU: State für Filter-Dialog
|
|
||||||
var showFilterDialog by remember { mutableStateOf(false) }
|
var showFilterDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var showDamageList by remember { mutableStateOf(false) }
|
var showDamageList by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
@@ -150,15 +175,15 @@ fun ContentScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter Dialog - NEU!
|
// Filter Dialog - AKTUALISIERT mit Status-Parameter
|
||||||
if (showFilterDialog) {
|
if (showFilterDialog) {
|
||||||
DamageFilterDialog(
|
DamageFilterDialog(
|
||||||
damageTypes = MapViewModel.DAMAGE_TYPES, // <-- Nutzt zentrale Liste
|
damageTypes = MapViewModel.DAMAGE_TYPES,
|
||||||
currentFilters = mapViewModel.getActiveFilters(),
|
currentFilters = mapViewModel.getActiveFilters(),
|
||||||
onDismiss = { showFilterDialog = false },
|
onDismiss = { showFilterDialog = false },
|
||||||
onApplyFilter = { selectedTypes, startDate, endDate ->
|
onApplyFilter = { selectedTypes, selectedStatus, startDate, endDate ->
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
mapViewModel.applyDamageFilter(selectedTypes, startDate, endDate)
|
mapViewModel.applyDamageFilter(selectedTypes, selectedStatus, startDate, endDate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -172,12 +197,16 @@ fun ContentScreen(
|
|||||||
modifier = Modifier.padding(bottom = 12.dp)
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SliderMenuItem(
|
||||||
|
text = "Einstellungen",
|
||||||
|
icon = Icons.Default.Settings,
|
||||||
|
onClick = onOpenSettings
|
||||||
|
)
|
||||||
|
|
||||||
SliderMenuItem(
|
SliderMenuItem(
|
||||||
text = "Schäden filtern",
|
text = "Schäden filtern",
|
||||||
icon = Icons.Default.FilterAlt,
|
icon = Icons.Default.FilterAlt,
|
||||||
onClick = {
|
onClick = { showFilterDialog = true }
|
||||||
showFilterDialog = true // <-- Öffnet Filter-Dialog
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
SliderMenuItem(
|
SliderMenuItem(
|
||||||
@@ -224,5 +253,4 @@ 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()
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -423,6 +423,10 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
fun clearDuplicateDamages() {
|
fun clearDuplicateDamages() {
|
||||||
duplicateDamages = emptyList()
|
duplicateDamages = emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getFeatureTable(): ServiceFeatureTable? {
|
||||||
|
return featureLayer?.featureTable as? ServiceFeatureTable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class FeatureOperationType(val operationName: String, val instruction: String) {
|
enum class FeatureOperationType(val operationName: String, val instruction: String) {
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
package com.example.snapandsolve.service
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import com.arcgismaps.data.ArcGISFeature
|
||||||
|
import com.arcgismaps.data.QueryParameters
|
||||||
|
import com.arcgismaps.data.ServiceFeatureTable
|
||||||
|
import com.arcgismaps.geometry.GeometryEngine
|
||||||
|
import com.arcgismaps.geometry.GeodeticCurveType
|
||||||
|
import com.arcgismaps.geometry.LinearUnit
|
||||||
|
import com.arcgismaps.geometry.LinearUnitId
|
||||||
|
import com.arcgismaps.geometry.Point
|
||||||
|
import com.arcgismaps.geometry.SpatialReference
|
||||||
|
import com.arcgismaps.location.SystemLocationDataSource
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
class ProximityNotificationService : Service() {
|
||||||
|
|
||||||
|
private var locationDataSource: SystemLocationDataSource? = null
|
||||||
|
private var featureTable: ServiceFeatureTable? = null
|
||||||
|
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
private val notifiedFeatures = mutableSetOf<String>()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CHANNEL_ID = "proximity_notifications"
|
||||||
|
const val NOTIFICATION_ID = 1001
|
||||||
|
const val FOREGROUND_SERVICE_ID = 1000
|
||||||
|
const val PROXIMITY_RADIUS_METERS = 100.0
|
||||||
|
|
||||||
|
private val _isRunning = MutableStateFlow(false)
|
||||||
|
val isRunning = _isRunning.asStateFlow()
|
||||||
|
|
||||||
|
private var sharedFeatureTable: ServiceFeatureTable? = null
|
||||||
|
|
||||||
|
fun start(context: Context, featureTable: ServiceFeatureTable) {
|
||||||
|
sharedFeatureTable = featureTable
|
||||||
|
val intent = Intent(context, ProximityNotificationService::class.java)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop(context: Context) {
|
||||||
|
sharedFeatureTable = null
|
||||||
|
context.stopService(Intent(context, ProximityNotificationService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
Log.d("ProximityService", "Service onCreate()")
|
||||||
|
|
||||||
|
createNotificationChannel()
|
||||||
|
startForeground(FOREGROUND_SERVICE_ID, createForegroundNotification())
|
||||||
|
_isRunning.value = true
|
||||||
|
|
||||||
|
featureTable = sharedFeatureTable
|
||||||
|
|
||||||
|
if (featureTable == null) {
|
||||||
|
Log.e("ProximityService", "FeatureTable is NULL in onCreate!")
|
||||||
|
} else {
|
||||||
|
Log.d("ProximityService", "FeatureTable successfully assigned")
|
||||||
|
Log.d("ProximityService", "FeatureTable SpatialReference: ${featureTable?.spatialReference?.wkid}")
|
||||||
|
}
|
||||||
|
|
||||||
|
startLocationTracking()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startLocationTracking() {
|
||||||
|
locationDataSource = SystemLocationDataSource().apply {
|
||||||
|
serviceScope.launch {
|
||||||
|
start().onSuccess {
|
||||||
|
Log.d("ProximityService", "Location tracking started")
|
||||||
|
locationChanged.collect { location ->
|
||||||
|
Log.d("ProximityService", "Location update: ${location.position.x}, ${location.position.y}")
|
||||||
|
checkProximityToDamages(location)
|
||||||
|
}
|
||||||
|
}.onFailure { error ->
|
||||||
|
Log.e("ProximityService", "Failed to start location tracking: ${error.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
Log.d("ProximityService", "Service onDestroy()")
|
||||||
|
serviceScope.launch {
|
||||||
|
locationDataSource?.stop()
|
||||||
|
}
|
||||||
|
serviceScope.cancel()
|
||||||
|
_isRunning.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"Straßenschaden-Benachrichtigungen",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
).apply {
|
||||||
|
description = "Benachrichtigungen über Straßenschäden in Ihrer Nähe"
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createForegroundNotification(): Notification {
|
||||||
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle("Straßenschaden-Überwachung")
|
||||||
|
.setContentText("Überwacht Straßenschäden in Ihrer Nähe")
|
||||||
|
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun checkProximityToDamages(location: com.arcgismaps.location.Location) {
|
||||||
|
Log.d("ProximityService", "Checking proximity at: ${location.position.x}, ${location.position.y}")
|
||||||
|
|
||||||
|
if (featureTable == null) {
|
||||||
|
Log.e("ProximityService", "FeatureTable is NULL! Cannot check proximity")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPS Position (WGS84)
|
||||||
|
val currentPoint = Point(
|
||||||
|
location.position.x,
|
||||||
|
location.position.y,
|
||||||
|
SpatialReference.wgs84()
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.d("ProximityService", "Current point SR: ${currentPoint.spatialReference?.wkid}")
|
||||||
|
|
||||||
|
val queryParams = QueryParameters().apply {
|
||||||
|
whereClause = "1=1"
|
||||||
|
returnGeometry = true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val queryResult = featureTable?.queryFeatures(queryParams)?.getOrNull()
|
||||||
|
val features = queryResult?.map { it as ArcGISFeature } ?: emptyList()
|
||||||
|
|
||||||
|
Log.d("ProximityService", "Found ${features.size} features total")
|
||||||
|
|
||||||
|
features.forEach { feature ->
|
||||||
|
val featureGeometry = feature.geometry as? Point ?: return@forEach
|
||||||
|
|
||||||
|
Log.d("ProximityService", "Feature point SR: ${featureGeometry.spatialReference?.wkid}")
|
||||||
|
|
||||||
|
// KRITISCHER FIX: Beide Punkte ins gleiche Koordinatensystem bringen
|
||||||
|
val projectedCurrentPoint = if (currentPoint.spatialReference?.wkid != featureGeometry.spatialReference?.wkid) {
|
||||||
|
GeometryEngine.projectOrNull(currentPoint, featureGeometry.spatialReference!!) as? Point
|
||||||
|
} else {
|
||||||
|
currentPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectedCurrentPoint == null) {
|
||||||
|
Log.e("ProximityService", "Failed to project current point")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
// KORRIGIERTE Distanzberechnung mit GeometryEngine
|
||||||
|
val distanceResult = GeometryEngine.distanceGeodeticOrNull(
|
||||||
|
point1 = projectedCurrentPoint,
|
||||||
|
point2 = featureGeometry,
|
||||||
|
distanceUnit = LinearUnit(LinearUnitId.Meters),
|
||||||
|
azimuthUnit = null,
|
||||||
|
curveType = GeodeticCurveType.Geodesic
|
||||||
|
)
|
||||||
|
|
||||||
|
val distance = distanceResult?.distance ?: return@forEach
|
||||||
|
|
||||||
|
Log.d("ProximityService", "Feature distance: ${String.format("%.2f", distance)}m")
|
||||||
|
|
||||||
|
if (distance <= PROXIMITY_RADIUS_METERS) {
|
||||||
|
val featureId = feature.attributes["OBJECTID"]?.toString() ?: return@forEach
|
||||||
|
|
||||||
|
if (!notifiedFeatures.contains(featureId)) {
|
||||||
|
Log.d("ProximityService", "Sending notification for feature $featureId at ${String.format("%.2f", distance)}m")
|
||||||
|
notifiedFeatures.add(featureId)
|
||||||
|
sendDamageNotification(feature, distance)
|
||||||
|
} else {
|
||||||
|
Log.d("ProximityService", "Already notified for feature $featureId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupNotifiedFeatures(currentPoint, features)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ProximityService", "Error checking proximity", e)
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendDamageNotification(feature: ArcGISFeature, distance: Double) {
|
||||||
|
// Versuche verschiedene Attribut-Namen für die Kategorie
|
||||||
|
val kategorie = feature.attributes["Kategorie"]?.toString()
|
||||||
|
?: feature.attributes["kategorie"]?.toString()
|
||||||
|
?: feature.attributes["Category"]?.toString()
|
||||||
|
?: feature.attributes["category"]?.toString()
|
||||||
|
?: "Straßenschaden" // Fallback
|
||||||
|
|
||||||
|
// Versuche verschiedene Attribut-Namen für die Beschreibung
|
||||||
|
val beschreibung = feature.attributes["Beschreibung"]?.toString()
|
||||||
|
?: feature.attributes["beschreibung"]?.toString()
|
||||||
|
?: feature.attributes["Description"]?.toString()
|
||||||
|
?: feature.attributes["description"]?.toString()
|
||||||
|
|
||||||
|
val distanceText = String.format("%.0f", distance)
|
||||||
|
|
||||||
|
// Baue den Notification-Text
|
||||||
|
val notificationText = if (beschreibung != null && beschreibung.isNotEmpty()) {
|
||||||
|
"$kategorie: $beschreibung - ca. ${distanceText}m entfernt"
|
||||||
|
} else {
|
||||||
|
"$kategorie - ca. ${distanceText}m entfernt"
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("ProximityService", "Notification text: $notificationText")
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle("⚠️ Straßenschaden in Ihrer Nähe!")
|
||||||
|
.setContentText(notificationText)
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle().bigText(notificationText)) // Für längere Texte
|
||||||
|
.setSmallIcon(android.R.drawable.ic_dialog_alert)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val notificationId = feature.attributes["OBJECTID"]?.toString()?.hashCode() ?: NOTIFICATION_ID
|
||||||
|
|
||||||
|
Log.d("ProximityService", "Displaying notification for: $kategorie")
|
||||||
|
notificationManager.notify(notificationId, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanupNotifiedFeatures(currentPoint: Point, features: List<ArcGISFeature>) {
|
||||||
|
val featuresToRemove = mutableListOf<String>()
|
||||||
|
|
||||||
|
notifiedFeatures.forEach { featureId ->
|
||||||
|
val feature = features.find { (it.attributes["OBJECTID"]?.toString() ?: "") == featureId }
|
||||||
|
if (feature == null) {
|
||||||
|
featuresToRemove.add(featureId)
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
val featureGeometry = feature.geometry as? Point ?: return@forEach
|
||||||
|
|
||||||
|
val projectedCurrentPoint = if (currentPoint.spatialReference?.wkid != featureGeometry.spatialReference?.wkid) {
|
||||||
|
GeometryEngine.projectOrNull(currentPoint, featureGeometry.spatialReference!!) as? Point
|
||||||
|
} else {
|
||||||
|
currentPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectedCurrentPoint == null) return@forEach
|
||||||
|
|
||||||
|
val distanceResult = GeometryEngine.distanceGeodeticOrNull(
|
||||||
|
point1 = projectedCurrentPoint,
|
||||||
|
point2 = featureGeometry,
|
||||||
|
distanceUnit = LinearUnit(LinearUnitId.Meters),
|
||||||
|
azimuthUnit = null,
|
||||||
|
curveType = GeodeticCurveType.Geodesic
|
||||||
|
)
|
||||||
|
|
||||||
|
val distance = distanceResult?.distance ?: return@forEach
|
||||||
|
|
||||||
|
if (distance > PROXIMITY_RADIUS_METERS + 50) {
|
||||||
|
featuresToRemove.add(featureId)
|
||||||
|
Log.d("ProximityService", "Removing feature $featureId from cache (distance: ${String.format("%.2f", distance)}m)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifiedFeatures.removeAll(featuresToRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,8 @@ import java.time.format.DateTimeFormatter
|
|||||||
fun FilterCheckboxItem(
|
fun FilterCheckboxItem(
|
||||||
label: String,
|
label: String,
|
||||||
isChecked: Boolean,
|
isChecked: Boolean,
|
||||||
onCheckedChange: (Boolean) -> Unit
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
emoji: String = "•"
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -47,14 +48,7 @@ fun FilterCheckboxItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = when (label) {
|
text = emoji,
|
||||||
"Straße" -> "🛣️"
|
|
||||||
"Gehweg" -> "🚶"
|
|
||||||
"Fahrradweg" -> "🚴"
|
|
||||||
"Beleuchtung" -> "💡"
|
|
||||||
"Sonstiges" -> "📍"
|
|
||||||
else -> "•"
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.headlineSmall
|
style = MaterialTheme.typography.headlineSmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -62,22 +56,28 @@ fun FilterCheckboxItem(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 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, Status UND Datum (unabhängig voneinander)
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun DamageFilterDialog(
|
fun DamageFilterDialog(
|
||||||
damageTypes: List<String>,
|
damageTypes: List<String>,
|
||||||
currentFilters: Set<String>,
|
currentFilters: Set<String>,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onApplyFilter: (Set<String>, LocalDate?, LocalDate?) -> Unit
|
onApplyFilter: (Set<String>, Set<String>, LocalDate?, LocalDate?) -> Unit
|
||||||
) {
|
) {
|
||||||
|
// Status-Liste
|
||||||
|
val statusTypes = listOf("neu", "in Bearbeitung", "Schaden behoben")
|
||||||
|
|
||||||
// WICHTIG: Wenn keine Filter aktiv sind, alle Typen standardmäßig auswählen!
|
// WICHTIG: Wenn keine Filter aktiv sind, alle Typen standardmäßig auswählen!
|
||||||
// Das ermöglicht Datumfilterung ohne Typ-Auswahl
|
|
||||||
var selectedFilters by remember {
|
var selectedFilters by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
if (currentFilters.isEmpty()) damageTypes.toSet() else currentFilters
|
if (currentFilters.isEmpty()) damageTypes.toSet() else currentFilters
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status-Filter: Standardmäßig alle ausgewählt
|
||||||
|
var selectedStatus by remember { mutableStateOf(statusTypes.toSet()) }
|
||||||
|
|
||||||
var startDateString by remember { mutableStateOf("") }
|
var startDateString by remember { mutableStateOf("") }
|
||||||
var endDateString by remember { mutableStateOf("") }
|
var endDateString by remember { mutableStateOf("") }
|
||||||
var startDate by remember { mutableStateOf<LocalDate?>(null) }
|
var startDate by remember { mutableStateOf<LocalDate?>(null) }
|
||||||
@@ -147,6 +147,51 @@ fun DamageFilterDialog(
|
|||||||
} else {
|
} else {
|
||||||
selectedFilters - type
|
selectedFilters - type
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
emoji = when (type) {
|
||||||
|
"Straße" -> "🛣️"
|
||||||
|
"Gehweg" -> "🚶"
|
||||||
|
"Fahrradweg" -> "🚴"
|
||||||
|
"Beleuchtung" -> "💡"
|
||||||
|
"Sonstiges" -> "📍"
|
||||||
|
else -> "•"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
|
||||||
|
// ===== STATUS-FILTER =====
|
||||||
|
Text(
|
||||||
|
text = "Bearbeitungsstatus:",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Wähle die Status aus, die angezeigt werden sollen:",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Status-Liste
|
||||||
|
statusTypes.forEach { status ->
|
||||||
|
FilterCheckboxItem(
|
||||||
|
label = status,
|
||||||
|
isChecked = selectedStatus.contains(status),
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
selectedStatus = if (isChecked) {
|
||||||
|
selectedStatus + status
|
||||||
|
} else {
|
||||||
|
selectedStatus - status
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emoji = when (status) {
|
||||||
|
"neu" -> "🔴"
|
||||||
|
"in Bearbeitung" -> "🟠"
|
||||||
|
"Schaden behoben" -> "🟢"
|
||||||
|
else -> "⚪"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -246,17 +291,26 @@ fun DamageFilterDialog(
|
|||||||
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
|
||||||
// Info über aktive Filter
|
// Info über aktive Filter
|
||||||
if (selectedFilters.isNotEmpty() || useDateFilter) {
|
if (selectedFilters.isNotEmpty() || selectedStatus.size < statusTypes.size || useDateFilter) {
|
||||||
Text(
|
Text(
|
||||||
text = buildString {
|
text = buildString {
|
||||||
|
// Typ-Info
|
||||||
if (selectedFilters.size < damageTypes.size) {
|
if (selectedFilters.size < damageTypes.size) {
|
||||||
append("${selectedFilters.size} von ${damageTypes.size} Typ(en)")
|
append("${selectedFilters.size} von ${damageTypes.size} Typ(en)")
|
||||||
} else {
|
} else {
|
||||||
append("Alle Typen")
|
append("Alle Typen")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status-Info
|
||||||
|
if (selectedStatus.size < statusTypes.size) {
|
||||||
|
append(" | ${selectedStatus.size} von ${statusTypes.size} Status")
|
||||||
|
} else {
|
||||||
|
append(" | Alle Status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datum-Info
|
||||||
if (useDateFilter) {
|
if (useDateFilter) {
|
||||||
if (selectedFilters.isNotEmpty()) append(" | ")
|
append(" | Datum: ${startDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"} - ${endDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"}")
|
||||||
append("Datum: ${startDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"} - ${endDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"}")
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
@@ -268,22 +322,28 @@ fun DamageFilterDialog(
|
|||||||
// Buttons
|
// Buttons
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
// Alle auswählen
|
// Alle Typen
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { selectedFilters = damageTypes.toSet() },
|
onClick = {
|
||||||
|
selectedFilters = damageTypes.toSet()
|
||||||
|
selectedStatus = statusTypes.toSet()
|
||||||
|
},
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("Alle Typen")
|
Text("Alle", maxLines = 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alle abwählen
|
// Keine Typen
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { selectedFilters = emptySet() },
|
onClick = {
|
||||||
|
selectedFilters = emptySet()
|
||||||
|
selectedStatus = emptySet()
|
||||||
|
},
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("Keine")
|
Text("Keine", maxLines = 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,14 +354,14 @@ fun DamageFilterDialog(
|
|||||||
onClick = {
|
onClick = {
|
||||||
val startDateToApply = if (useDateFilter) startDate else null
|
val startDateToApply = if (useDateFilter) startDate else null
|
||||||
val endDateToApply = if (useDateFilter) endDate else null
|
val endDateToApply = if (useDateFilter) endDate else null
|
||||||
onApplyFilter(selectedFilters, startDateToApply, endDateToApply)
|
onApplyFilter(selectedFilters, selectedStatus, startDateToApply, endDateToApply)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
when {
|
when {
|
||||||
selectedFilters.isEmpty() && !useDateFilter -> "Alle anzeigen"
|
selectedFilters.isEmpty() && selectedStatus.isEmpty() && !useDateFilter -> "Alle anzeigen"
|
||||||
else -> "Filter anwenden"
|
else -> "Filter anwenden"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -313,25 +373,34 @@ fun DamageFilterDialog(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 + Status + Datum - UNABHÄNGIG)
|
||||||
*/
|
*/
|
||||||
suspend fun MapViewModel.applyDamageFilter(
|
suspend fun MapViewModel.applyDamageFilter(
|
||||||
selectedTypes: Set<String>,
|
selectedTypes: Set<String>,
|
||||||
|
selectedStatus: Set<String>,
|
||||||
startDate: LocalDate? = null,
|
startDate: LocalDate? = null,
|
||||||
endDate: LocalDate? = null
|
endDate: LocalDate? = null
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
val statusTypes = listOf("neu", "in Bearbeitung", "Schaden behoben")
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val whereClauses = mutableListOf<String>()
|
val whereClauses = mutableListOf<String>()
|
||||||
|
|
||||||
// ===== TYP-FILTER =====
|
// ===== TYP-FILTER =====
|
||||||
// WICHTIG: Nur hinzufügen wenn nicht ALLE Typen ausgewählt sind
|
// WICHTIG: Nur hinzufügen wenn nicht ALLE Typen ausgewählt sind
|
||||||
// Wenn alle ausgewählt sind, filtere nicht nach Typ (ermöglicht reinen Datumsfilter)
|
|
||||||
if (selectedTypes.isNotEmpty() && selectedTypes.size < DAMAGE_TYPES.size) {
|
if (selectedTypes.isNotEmpty() && selectedTypes.size < DAMAGE_TYPES.size) {
|
||||||
val typeList = selectedTypes.joinToString("', '", "'", "'")
|
val typeList = selectedTypes.joinToString("', '", "'", "'")
|
||||||
whereClauses.add("Typ IN ($typeList)")
|
whereClauses.add("Typ IN ($typeList)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== STATUS-FILTER =====
|
||||||
|
// Nur hinzufügen wenn nicht ALLE Status ausgewählt sind
|
||||||
|
if (selectedStatus.isNotEmpty() && selectedStatus.size < statusTypes.size) {
|
||||||
|
val statusList = selectedStatus.joinToString("', '", "'", "'")
|
||||||
|
whereClauses.add("status IN ($statusList)")
|
||||||
|
}
|
||||||
|
|
||||||
// ===== DATUMS-FILTER =====
|
// ===== DATUMS-FILTER =====
|
||||||
// Feldname: EditDate
|
// Feldname: EditDate
|
||||||
if (startDate != null || endDate != null) {
|
if (startDate != null || endDate != null) {
|
||||||
@@ -355,26 +424,32 @@ suspend fun MapViewModel.applyDamageFilter(
|
|||||||
|
|
||||||
featureLayer.definitionExpression = whereClause
|
featureLayer.definitionExpression = whereClause
|
||||||
println("DEBUG: Filter angewendet: '$whereClause'")
|
println("DEBUG: Filter angewendet: '$whereClause'")
|
||||||
println("DEBUG: Typen: ${selectedTypes.size}/${DAMAGE_TYPES.size}, Datum aktiv: ${startDate != null || endDate != null}")
|
println("DEBUG: Typen: ${selectedTypes.size}/${DAMAGE_TYPES.size}, Status: ${selectedStatus.size}/${statusTypes.size}, Datum aktiv: ${startDate != null || endDate != null}")
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
snackBarMessage = when {
|
snackBarMessage = buildString {
|
||||||
selectedTypes.isEmpty() && (startDate == null && endDate == null) -> {
|
val parts = mutableListOf<String>()
|
||||||
"Alle Schäden werden angezeigt"
|
|
||||||
|
// Typ-Info
|
||||||
|
if (selectedTypes.size < DAMAGE_TYPES.size) {
|
||||||
|
parts.add("${selectedTypes.size} Typ(en)")
|
||||||
}
|
}
|
||||||
selectedTypes.size == DAMAGE_TYPES.size && (startDate != null || endDate != null) -> {
|
|
||||||
// NUR Datumsfilter aktiv (alle Typen ausgewählt)
|
// Status-Info
|
||||||
"Datumsfilter: ${startDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"} - ${endDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"}"
|
if (selectedStatus.size < statusTypes.size) {
|
||||||
|
parts.add("${selectedStatus.size} Status")
|
||||||
}
|
}
|
||||||
selectedTypes.size < DAMAGE_TYPES.size && (startDate == null && endDate == null) -> {
|
|
||||||
// NUR Typ-Filter aktiv
|
// Datum-Info
|
||||||
"${selectedTypes.size} Typ(en) gefiltert"
|
if (startDate != null || endDate != null) {
|
||||||
|
val dateStr = "${startDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"} - ${endDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"}"
|
||||||
|
parts.add("Datum: $dateStr")
|
||||||
}
|
}
|
||||||
else -> {
|
|
||||||
// BEIDE Filter aktiv
|
if (parts.isEmpty()) {
|
||||||
val typeInfo = "${selectedTypes.size} Typ(en)"
|
append("Alle Schäden werden angezeigt")
|
||||||
val dateInfo = "Datum: ${startDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"} - ${endDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"}"
|
} else {
|
||||||
"$typeInfo | $dateInfo"
|
append("Filter: ${parts.joinToString(" | ")}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package com.example.snapandsolve.ui.theme
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.example.snapandsolve.service.ProximityNotificationService // KORREKTER IMPORT
|
||||||
|
import MapViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
mapViewModel: MapViewModel
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val isProximityActive by ProximityNotificationService.isRunning.collectAsState()
|
||||||
|
|
||||||
|
// NEU: Notification Permission Launcher
|
||||||
|
val notificationPermissionLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted ->
|
||||||
|
if (isGranted) {
|
||||||
|
Toast.makeText(context, "Benachrichtigungen aktiviert", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Benachrichtigungen verweigert", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Einstellungen") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowBack,
|
||||||
|
contentDescription = "Zurück"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = AppColor,
|
||||||
|
titleContentColor = Color.White,
|
||||||
|
navigationIconContentColor = Color.White
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// Benachrichtigungen Section
|
||||||
|
Text(
|
||||||
|
text = "Benachrichtigungen",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = Color.Gray,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = WidgetColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Notifications,
|
||||||
|
contentDescription = "Benachrichtigungen",
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "Straßenschäden in der Nähe",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Benachrichtigung bei Schäden im Umkreis von 100m",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.Gray,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Switch(
|
||||||
|
checked = isProximityActive,
|
||||||
|
onCheckedChange = { enabled ->
|
||||||
|
if (enabled) {
|
||||||
|
// NEU: Prüfe Notification Permission (Android 13+)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
val hasPermission = context.checkSelfPermission(
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
return@Switch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mapViewModel.getFeatureTable()?.let { table ->
|
||||||
|
ProximityNotificationService.start(context, table)
|
||||||
|
} ?: run {
|
||||||
|
Toast.makeText(context, "Feature Table nicht verfügbar", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ProximityNotificationService.stop(context)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = Color.White,
|
||||||
|
checkedTrackColor = ButtonColor,
|
||||||
|
uncheckedThumbColor = Color.White,
|
||||||
|
uncheckedTrackColor = Color.LightGray
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status-Anzeige
|
||||||
|
if (isProximityActive) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = ButtonColor.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "✓",
|
||||||
|
color = ButtonColor,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Benachrichtigungen sind aktiv",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = ButtonColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Weitere Einstellungen können hier hinzugefügt werden
|
||||||
|
Text(
|
||||||
|
text = "Informationen",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = Color.Gray,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = WidgetColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Proximity-Radius",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "100 Meter",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color.Gray,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
app/src/main/res/drawable/ic_notification.xml
Normal file
10
app/src/main/res/drawable/ic_notification.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
|
||||||
|
</vector>
|
||||||
Reference in New Issue
Block a user