forked from jo1042/StrassenSchadenPro2
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5f8746fc4 | |||
| 1c5d43bb02 | |||
| 0194aabe62 | |||
| 0c045c8c91 | |||
| bd66f298e0 | |||
| 56c61d38a1 | |||
| 549cf73ede | |||
| 3c1c91b5b7 | |||
| 78b5e0264e | |||
| 04fd485cf9 | |||
| c05ea2878e |
@@ -2,6 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -18,6 +19,14 @@ android {
|
|||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
val properties = org.jetbrains.kotlin.konan.properties.Properties()
|
||||||
|
val propertiesFile = rootProject.file("local.properties")
|
||||||
|
if (propertiesFile.exists()){
|
||||||
|
propertiesFile.inputStream().use { properties.load(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConfigField("String", "ARCGIS_TOKEN","\"${properties.getProperty("arcgis.token","")}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -38,6 +47,7 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,4 +67,13 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
||||||
|
implementation("com.google.android.gms:play-services-location:21.3.0")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
|
||||||
|
// ArcGIS Maps for Kotlin - SDK dependency
|
||||||
|
implementation(libs.arcgis.maps.kotlin)
|
||||||
|
// Toolkit dependencies
|
||||||
|
implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom))
|
||||||
|
implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose)
|
||||||
|
implementation(libs.arcgis.maps.kotlin.toolkit.authentication)
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,22 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package de.jadehs.strassenschadenpro2
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import de.jadehs.strassenschadenpro2.components.album.AlbumViewState
|
||||||
|
import de.jadehs.strassenschadenpro2.components.album.Intent
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class AlbumViewModel(private val coroutineContext: CoroutineContext): ViewModel() {
|
||||||
|
//region View State
|
||||||
|
private val _albumViewState: MutableStateFlow<AlbumViewState> = MutableStateFlow(AlbumViewState())
|
||||||
|
// exposes the ViewState to the composable view
|
||||||
|
val viewStateFlow: StateFlow<AlbumViewState>
|
||||||
|
get() = _albumViewState
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Intents
|
||||||
|
// receives user generated events and processes them in the provided coroutine context
|
||||||
|
fun onReceive(intent: Intent) = viewModelScope.launch(coroutineContext) {
|
||||||
|
when(intent) {
|
||||||
|
is Intent.OnPermissionGrantedWith -> {
|
||||||
|
// Create an empty image file in the app's cache directory
|
||||||
|
val tempFile = File.createTempFile(
|
||||||
|
"temp_image_file_", /* prefix */
|
||||||
|
".jpg", /* suffix */
|
||||||
|
intent.compositionContext.cacheDir /* cache directory */
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create sandboxed url for this temp file - needed for the camera API
|
||||||
|
val uri = FileProvider.getUriForFile(intent.compositionContext,
|
||||||
|
"${BuildConfig.APPLICATION_ID}.provider", /* needs to match the provider information in the manifest */
|
||||||
|
tempFile
|
||||||
|
)
|
||||||
|
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Intent.OnPermissionDenied -> {
|
||||||
|
// maybe log the permission denial event
|
||||||
|
println("User did not grant permission to use the camera")
|
||||||
|
}
|
||||||
|
|
||||||
|
is Intent.OnFinishPickingImagesWith -> {
|
||||||
|
if (intent.imageUrls.isNotEmpty()) {
|
||||||
|
// Handle picked images
|
||||||
|
val newImages = mutableListOf<ImageBitmap>()
|
||||||
|
val imageUriList = _albumViewState.value.imageUris.toMutableList()
|
||||||
|
for (eachImageUrl in intent.imageUrls) {
|
||||||
|
val inputStream = intent.compositionContext.contentResolver.openInputStream(eachImageUrl)
|
||||||
|
val bytes = inputStream?.readBytes()
|
||||||
|
inputStream?.close()
|
||||||
|
|
||||||
|
if (bytes != null) {
|
||||||
|
val bitmapOptions = BitmapFactory.Options()
|
||||||
|
bitmapOptions.inMutable = true
|
||||||
|
val bitmap: Bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bitmapOptions)
|
||||||
|
newImages.add(bitmap.asImageBitmap())
|
||||||
|
imageUriList.add(eachImageUrl)
|
||||||
|
} else {
|
||||||
|
// error reading the bytes from the image url
|
||||||
|
println("The image that was picked could not be read from the device at this url: $eachImageUrl")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentViewState = _albumViewState.value
|
||||||
|
val newCopy = currentViewState.copy(
|
||||||
|
selectedPictures = (currentViewState.selectedPictures + newImages),
|
||||||
|
tempFileUrl = null,
|
||||||
|
imageUris = imageUriList
|
||||||
|
)
|
||||||
|
_albumViewState.value = newCopy
|
||||||
|
} else {
|
||||||
|
// user did not pick anything
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Intent.OnImageSavedWith -> {
|
||||||
|
val tempImageUrl = _albumViewState.value.tempFileUrl
|
||||||
|
if (tempImageUrl != null) {
|
||||||
|
val source = ImageDecoder.createSource(intent.compositionContext.contentResolver, tempImageUrl)
|
||||||
|
|
||||||
|
val currentPictures = _albumViewState.value.selectedPictures.toMutableList()
|
||||||
|
currentPictures.add(ImageDecoder.decodeBitmap(source).asImageBitmap())
|
||||||
|
|
||||||
|
val photoUriList = _albumViewState.value.imageUris.toMutableList()
|
||||||
|
photoUriList.add(tempImageUrl)
|
||||||
|
|
||||||
|
_albumViewState.value = _albumViewState.value.copy(
|
||||||
|
tempFileUrl = null,
|
||||||
|
selectedPictures = currentPictures,
|
||||||
|
imageUris = photoUriList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Intent.OnImageSavingCanceled -> {
|
||||||
|
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
}
|
||||||
105
app/src/main/java/de/jadehs/strassenschadenpro2/ListViewModel.kt
Normal file
105
app/src/main/java/de/jadehs/strassenschadenpro2/ListViewModel.kt
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package de.jadehs.strassenschadenpro2
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class ListViewModel(application: Application): AndroidViewModel(application) {
|
||||||
|
|
||||||
|
private val _features = MutableStateFlow<List<Feature>>(emptyList())
|
||||||
|
val features: StateFlow<List<Feature>> = _features
|
||||||
|
|
||||||
|
fun fetch() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val data =
|
||||||
|
loadFeatures("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/ArcGIS/rest/services/StrassenSchaeden/FeatureServer/0/query?where=1%3D1&objectIds=&geometry=&geometryType=esriGeometryEnvelope&inSR=&spatialRel=esriSpatialRelIntersects&resultType=none&distance=0.0&units=esriSRUnit_Meter&outDistance=&relationParam=&returnGeodetic=false&outFields=OBJECTID%2Ctyp%2Cbeschreibung&returnGeometry=true&featureEncoding=esriDefault&multipatchOption=xyFootprint&maxAllowableOffset=&geometryPrecision=&outSR=&defaultSR=&datumTransformation=&applyVCSProjection=false&returnIdsOnly=false&returnUniqueIdsOnly=false&returnCountOnly=false&returnExtentOnly=false&returnQueryGeometry=false&returnDistinctValues=false&cacheHint=false&collation=&orderByFields=&groupByFieldsForStatistics=&returnAggIds=false&outStatistics=&having=&resultOffset=&resultRecordCount=&returnZ=false&returnM=false&returnTrueCurves=false&returnExceededLimitFeatures=true&quantizationParameters=&sqlFormat=none&f=pgeojson&token=")
|
||||||
|
_features.value = data.features
|
||||||
|
} catch (e: Exception){
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadFeatures(url: String): FeatureCollection {
|
||||||
|
val jsonParser = Json { ignoreUnknownKeys = true }
|
||||||
|
val jsonText = loadJsonFromUrl(url)
|
||||||
|
return jsonParser.decodeFromString(jsonText)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadJsonFromUrl(urlString: String): String{
|
||||||
|
return withContext(Dispatchers.IO){
|
||||||
|
val url = URL(urlString)
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
conn.requestMethod = "GET"
|
||||||
|
conn.connectTimeout = 5000
|
||||||
|
conn.readTimeout = 5000
|
||||||
|
|
||||||
|
try {
|
||||||
|
val stream = conn.inputStream
|
||||||
|
stream.bufferedReader().use { it.readText() }
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FeatureCollection(
|
||||||
|
val type: String? = null,
|
||||||
|
val features: List<Feature> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Feature(
|
||||||
|
val type: String? = null,
|
||||||
|
val id: Int? = null,
|
||||||
|
val geometry: Geometry? = null,
|
||||||
|
val properties: Properties? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Geometry(
|
||||||
|
val type: String? = null,
|
||||||
|
val coordinates: List<Double> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Properties(
|
||||||
|
val OBJECTID: Int? = null,
|
||||||
|
val Typ: String? = null,
|
||||||
|
val Beschreibung: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -34,9 +34,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
StrassenSchadenPro2Theme {
|
StrassenSchadenPro2Theme {
|
||||||
MainScreen()
|
MainScreen(application=application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package de.jadehs.strassenschadenpro2
|
package de.jadehs.strassenschadenpro2
|
||||||
|
|
||||||
import android.util.Log
|
import android.app.Application
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -25,7 +25,7 @@ import de.jadehs.strassenschadenpro2.pages.MapPage
|
|||||||
import de.jadehs.strassenschadenpro2.pages.SettingsPage
|
import de.jadehs.strassenschadenpro2.pages.SettingsPage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(modifier: Modifier = Modifier) {
|
fun MainScreen(modifier: Modifier = Modifier, application: Application) {
|
||||||
|
|
||||||
val navItemList = listOf(
|
val navItemList = listOf(
|
||||||
NavItem("Karte",Icons.Default.Place),
|
NavItem("Karte",Icons.Default.Place),
|
||||||
@@ -36,6 +36,11 @@ fun MainScreen(modifier: Modifier = Modifier) {
|
|||||||
|
|
||||||
var selectedIndex by remember { mutableStateOf(0) }
|
var selectedIndex by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
val mapViewModel = remember { MapViewModel(application) }
|
||||||
|
val listViewModel = remember { ListViewModel(application) }
|
||||||
|
|
||||||
|
var selectedObjectId by remember { mutableStateOf<Int?>(null) }
|
||||||
|
|
||||||
Scaffold(modifier = Modifier.fillMaxSize(),
|
Scaffold(modifier = Modifier.fillMaxSize(),
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
@@ -52,17 +57,36 @@ fun MainScreen(modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
innerPadding ->
|
innerPadding ->
|
||||||
ContentScreen(modifier = Modifier.padding(innerPadding), selectedIndex)
|
ContentScreen(modifier = Modifier.padding(innerPadding),
|
||||||
|
selectedIndex,
|
||||||
|
mapViewModel,
|
||||||
|
listViewModel,
|
||||||
|
selectedObjectId,
|
||||||
|
onFeatureSelected = { objectId ->
|
||||||
|
selectedObjectId = objectId
|
||||||
|
selectedIndex = 0
|
||||||
|
},
|
||||||
|
onObjectIdUsed = {
|
||||||
|
selectedObjectId = null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContentScreen(modifier: Modifier = Modifier, selectedIndex: Int) {
|
fun ContentScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selectedIndex: Int,
|
||||||
|
mapViewModel: MapViewModel,
|
||||||
|
listViewModel: ListViewModel,
|
||||||
|
selectedObjectId: Int?,
|
||||||
|
onFeatureSelected: (Int) -> Unit,
|
||||||
|
onObjectIdUsed: () -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
when(selectedIndex) {
|
when(selectedIndex) {
|
||||||
0 -> MapPage()
|
0 -> MapPage(modifier = modifier, mapViewModel = mapViewModel,selectedObjectId=selectedObjectId, onObjectIdUsed = onObjectIdUsed)
|
||||||
1 -> CreatePage()
|
1 -> CreatePage(modifier = modifier, mapViewModel = mapViewModel)
|
||||||
2 -> ListPage()
|
2 -> ListPage(modifier = modifier, listViewModel = listViewModel, onFeatureClick = onFeatureSelected)
|
||||||
3 -> SettingsPage()
|
3 -> SettingsPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
369
app/src/main/java/de/jadehs/strassenschadenpro2/MapViewModel.kt
Normal file
369
app/src/main/java/de/jadehs/strassenschadenpro2/MapViewModel.kt
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
package de.jadehs.strassenschadenpro2
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.arcgismaps.LoadStatus
|
||||||
|
import com.arcgismaps.data.ArcGISFeature
|
||||||
|
import com.arcgismaps.data.CodedValueDomain
|
||||||
|
import com.arcgismaps.data.QueryParameters
|
||||||
|
import com.arcgismaps.data.ServiceFeatureTable
|
||||||
|
import com.arcgismaps.geometry.GeometryEngine
|
||||||
|
import com.arcgismaps.geometry.Point
|
||||||
|
import com.arcgismaps.mapping.ArcGISMap
|
||||||
|
import com.arcgismaps.mapping.BasemapStyle
|
||||||
|
import com.arcgismaps.mapping.Viewpoint
|
||||||
|
import com.arcgismaps.mapping.layers.FeatureLayer
|
||||||
|
import com.arcgismaps.mapping.view.ScreenCoordinate
|
||||||
|
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
|
||||||
|
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
|
||||||
|
import de.jadehs.strassenschadenpro2.components.album.PhotoData
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class MapViewModel(application: Application): AndroidViewModel(application) {
|
||||||
|
val map: ArcGISMap = ArcGISMap(BasemapStyle.OpenOsmStyle).apply {
|
||||||
|
initialViewpoint = Viewpoint(53.14, 8.20, 20000.0)
|
||||||
|
}
|
||||||
|
// Hold a reference to the selected feature.
|
||||||
|
var selectedFeature: ArcGISFeature? by mutableStateOf(null)
|
||||||
|
val mapViewProxy = MapViewProxy()
|
||||||
|
|
||||||
|
var currentFeatureOperation by mutableStateOf(FeatureOperationType.CREATE)
|
||||||
|
|
||||||
|
lateinit var featureLayer: FeatureLayer
|
||||||
|
|
||||||
|
// Create a snackbar message to display the result of feature operations.
|
||||||
|
var snackBarMessage: String by mutableStateOf("")
|
||||||
|
|
||||||
|
lateinit var serviceFeatureTable: ServiceFeatureTable
|
||||||
|
|
||||||
|
var currentDamageType by mutableStateOf("")
|
||||||
|
|
||||||
|
// The list of damage types to update the feature attribute.
|
||||||
|
var damageTypeList: List<String> = mutableListOf()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/ArcGIS/rest/services/StrassenSchaeden/FeatureServer/0")
|
||||||
|
serviceFeatureTable.load().onSuccess {
|
||||||
|
// Get the field from the feature table that will be updated.
|
||||||
|
val typeDamageField = serviceFeatureTable.fields.first { it.name == "Typ" }
|
||||||
|
// Get the coded value domain for the field.
|
||||||
|
val attributeDomain = typeDamageField.domain as CodedValueDomain
|
||||||
|
// Add the damage types to the list.
|
||||||
|
attributeDomain.codedValues.forEach {
|
||||||
|
damageTypeList += it.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
featureLayer = FeatureLayer.createWithFeatureTable(serviceFeatureTable)
|
||||||
|
map.operationalLayers.add(featureLayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current feature operation to perform based on the selected index from the dropdown. Also, reset feature
|
||||||
|
* selection.
|
||||||
|
*/
|
||||||
|
fun onFeatureOperationSelected(index: Int) {
|
||||||
|
currentFeatureOperation = FeatureOperationType.entries[index]
|
||||||
|
// Reset the selected feature when the operation changes.
|
||||||
|
featureLayer?.clearSelection()
|
||||||
|
selectedFeature = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directs the behaviour of tap's on the map view.
|
||||||
|
*/
|
||||||
|
fun onTap(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
|
||||||
|
if (featureLayer?.loadStatus?.value != LoadStatus.Loaded) {
|
||||||
|
snackBarMessage = "Layer not loaded!"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
when (currentFeatureOperation) {
|
||||||
|
FeatureOperationType.CREATE -> createFeatureAt(singleTapConfirmedEvent.screenCoordinate)
|
||||||
|
FeatureOperationType.DELETE -> deleteFeatureAt(singleTapConfirmedEvent.screenCoordinate)
|
||||||
|
FeatureOperationType.UPDATE_ATTRIBUTE -> selectFeatureForAttributeEditAt(singleTapConfirmedEvent.screenCoordinate)
|
||||||
|
FeatureOperationType.UPDATE_GEOMETRY -> updateFeatureGeometryAt(singleTapConfirmedEvent.screenCoordinate)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new feature at the tapped location with some default attributes
|
||||||
|
*/
|
||||||
|
private fun createFeatureAt(screenCoordinate: ScreenCoordinate) {
|
||||||
|
// Create the feature.
|
||||||
|
val feature = serviceFeatureTable?.createFeature()?.apply {
|
||||||
|
// Get the normalized geometry for the tapped location and use it as the feature's geometry.
|
||||||
|
mapViewProxy.screenToLocationOrNull(screenCoordinate)?.let { mapPoint ->
|
||||||
|
geometry = GeometryEngine.normalizeCentralMeridian(mapPoint)
|
||||||
|
// Set feature attributes.
|
||||||
|
attributes["Typ"] = "SL"
|
||||||
|
attributes["Beschreibung"] = "Earthquake"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feature?.let {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Add the feature to the table.
|
||||||
|
serviceFeatureTable?.addFeature(it)
|
||||||
|
// Apply the edits to the service on the service geodatabase.
|
||||||
|
serviceFeatureTable?.applyEdits()
|
||||||
|
// Update the feature to get the updated objectid - a temporary ID is used before the feature is added.
|
||||||
|
it.refresh()
|
||||||
|
// Confirm feature addition.
|
||||||
|
snackBarMessage = "Created feature ${it.attributes["objectid"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createFeatureWithAttributesAt(screenCoordinate: ScreenCoordinate, beschreibung: String, typ: String,photosFromAlbum: List<PhotoData>) {
|
||||||
|
// Create the feature.
|
||||||
|
val feature = serviceFeatureTable?.createFeature()?.apply {
|
||||||
|
// Get the normalized geometry for the tapped location and use it as the feature's geometry.
|
||||||
|
mapViewProxy.screenToLocationOrNull(screenCoordinate)?.let { mapPoint ->
|
||||||
|
geometry = GeometryEngine.normalizeCentralMeridian(mapPoint)
|
||||||
|
// Set feature attributes.
|
||||||
|
attributes["Typ"] = typ
|
||||||
|
attributes["Beschreibung"] = beschreibung
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feature?.let {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Add the feature to the table.
|
||||||
|
serviceFeatureTable?.addFeature(it)
|
||||||
|
// Apply the edits to the service on the service geodatabase.
|
||||||
|
serviceFeatureTable?.applyEdits()
|
||||||
|
// Update the feature to get the updated objectid - a temporary ID is used before the feature is added.
|
||||||
|
it.refresh()
|
||||||
|
|
||||||
|
var arcgisFeature = it as? ArcGISFeature
|
||||||
|
if (arcgisFeature != null && photosFromAlbum.size>0){
|
||||||
|
for (photoData in photosFromAlbum){
|
||||||
|
arcgisFeature.addAttachment(photoData.name,photoData.contentType,photoData.data)
|
||||||
|
}
|
||||||
|
serviceFeatureTable?.updateFeature(arcgisFeature)
|
||||||
|
serviceFeatureTable?.applyEdits()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm feature addition.
|
||||||
|
snackBarMessage = "Created feature ${it.attributes["objectid"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createFeatureWithAttributesAtPoint(
|
||||||
|
point: Point,
|
||||||
|
beschreibung: String,
|
||||||
|
typ: String,
|
||||||
|
photoDataList: List<PhotoData>
|
||||||
|
) {
|
||||||
|
// Create the feature.
|
||||||
|
val feature = serviceFeatureTable?.createFeature()?.apply {
|
||||||
|
// Get the normalized geometry for the tapped location and use it as the feature's geometry.
|
||||||
|
|
||||||
|
geometry = GeometryEngine.normalizeCentralMeridian(point)
|
||||||
|
// Set feature attributes.
|
||||||
|
attributes["Typ"] = typ
|
||||||
|
attributes["Beschreibung"] = beschreibung
|
||||||
|
|
||||||
|
}
|
||||||
|
feature?.let {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Add the feature to the table.
|
||||||
|
serviceFeatureTable?.addFeature(it)
|
||||||
|
// Apply the edits to the service on the service geodatabase.
|
||||||
|
serviceFeatureTable?.applyEdits()
|
||||||
|
// Update the feature to get the updated objectid - a temporary ID is used before the feature is added.
|
||||||
|
it.refresh()
|
||||||
|
|
||||||
|
var arcgisFeature = it as? ArcGISFeature
|
||||||
|
if (arcgisFeature != null && photoDataList.size>0){
|
||||||
|
for (photoData in photoDataList){
|
||||||
|
arcgisFeature.addAttachment(photoData.name,photoData.contentType,photoData.data)
|
||||||
|
}
|
||||||
|
serviceFeatureTable?.updateFeature(arcgisFeature)
|
||||||
|
serviceFeatureTable?.applyEdits()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm feature addition.
|
||||||
|
snackBarMessage = "Created feature ${it.attributes["objectid"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a feature at the tapped location in preparation for deletion.
|
||||||
|
*/
|
||||||
|
private fun deleteFeatureAt(screenCoordinate: ScreenCoordinate) {
|
||||||
|
featureLayer?.let { featureLayer ->
|
||||||
|
// Clear any existing selection.
|
||||||
|
featureLayer.clearSelection()
|
||||||
|
selectedFeature = null
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Determine if a user tapped on a feature.
|
||||||
|
mapViewProxy.identify(featureLayer, screenCoordinate, 10.dp).onSuccess { identifyResult ->
|
||||||
|
selectedFeature = (identifyResult.geoElements.firstOrNull() as? ArcGISFeature)?.also {
|
||||||
|
featureLayer.selectFeature(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the selected feature from the feature table and service geodatabase.
|
||||||
|
*/
|
||||||
|
fun deleteSelectedFeature() {
|
||||||
|
selectedFeature?.let {
|
||||||
|
// Get the feature's object id.
|
||||||
|
val featureId = it.attributes["objectid"] as Long
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Delete the feature from the feature table.
|
||||||
|
serviceFeatureTable?.deleteFeature(it)?.onSuccess {
|
||||||
|
snackBarMessage = "Deleted feature $featureId"
|
||||||
|
// Apply the edits to the service geodatabase.
|
||||||
|
serviceFeatureTable?.applyEdits()
|
||||||
|
selectedFeature = null
|
||||||
|
}?.onFailure {
|
||||||
|
snackBarMessage = "Failed to delete feature $featureId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a feature at the tapped location in preparation for attribute editing.
|
||||||
|
*/
|
||||||
|
private fun selectFeatureForAttributeEditAt(screenCoordinate: ScreenCoordinate) {
|
||||||
|
featureLayer?.let { featureLayer ->
|
||||||
|
// Clear any existing selection.
|
||||||
|
featureLayer.clearSelection()
|
||||||
|
selectedFeature = null
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Determine if a user tapped on a feature.
|
||||||
|
mapViewProxy.identify(featureLayer, screenCoordinate, 10.dp).onSuccess { identifyResult ->
|
||||||
|
// Get the identified feature.
|
||||||
|
val identifiedFeature = identifyResult.geoElements.firstOrNull() as? ArcGISFeature
|
||||||
|
identifiedFeature?.let {
|
||||||
|
val currentAttributeValue = it.attributes["Typ"] as String
|
||||||
|
currentDamageType = currentAttributeValue
|
||||||
|
selectedFeature = it.also {
|
||||||
|
featureLayer.selectFeature(it)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
// Reset damage type if no feature identified.
|
||||||
|
currentDamageType = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the attribute value of the selected feature to the new value from the new damage type.
|
||||||
|
*/
|
||||||
|
fun onDamageTypeSelected(index: Int) {
|
||||||
|
// Get the new value.
|
||||||
|
currentDamageType = damageTypeList[index]
|
||||||
|
selectedFeature?.let { selectedFeature ->
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Load the feature.
|
||||||
|
selectedFeature.load().onSuccess {
|
||||||
|
// Update the attribute value.
|
||||||
|
selectedFeature.attributes["Typ"] = currentDamageType
|
||||||
|
// Update the table.
|
||||||
|
serviceFeatureTable?.updateFeature(selectedFeature)
|
||||||
|
// Update the service on the service geodatabase.
|
||||||
|
serviceFeatureTable?.applyEdits()?.onSuccess {
|
||||||
|
snackBarMessage =
|
||||||
|
"Updated feature ${selectedFeature.attributes["objectid"]} to $currentDamageType"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a feature, if none is selected. If a feature is selected, update its geometry to the tapped location.
|
||||||
|
*/
|
||||||
|
private fun updateFeatureGeometryAt(screenCoordinate: ScreenCoordinate) {
|
||||||
|
|
||||||
|
featureLayer?.let { featureLayer ->
|
||||||
|
when (selectedFeature) {
|
||||||
|
// When no feature is selected.
|
||||||
|
null -> {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Determine if a user tapped on a feature.
|
||||||
|
mapViewProxy.identify(featureLayer, screenCoordinate, 10.dp).onSuccess { identifyResult ->
|
||||||
|
// Get the identified feature.
|
||||||
|
val identifiedFeature = identifyResult.geoElements.firstOrNull() as? ArcGISFeature
|
||||||
|
identifiedFeature?.let {
|
||||||
|
selectedFeature = it.also {
|
||||||
|
featureLayer.selectFeature(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// When a feature is selected, update its geometry to the tapped location.
|
||||||
|
else -> {
|
||||||
|
mapViewProxy.screenToLocationOrNull(screenCoordinate)?.let { mapPoint ->
|
||||||
|
// Normalize the point - needed when the tapped location is over the international date line.
|
||||||
|
val destinationPoint = GeometryEngine.normalizeCentralMeridian(mapPoint)
|
||||||
|
viewModelScope.launch {
|
||||||
|
selectedFeature?.let { selectedFeature ->
|
||||||
|
// Load the feature.
|
||||||
|
selectedFeature.load().onSuccess {
|
||||||
|
// Update the geometry of the selected feature.
|
||||||
|
selectedFeature.geometry = destinationPoint
|
||||||
|
// Apply the edit to the feature table.
|
||||||
|
serviceFeatureTable?.updateFeature(selectedFeature)
|
||||||
|
// Push the update to the service with the service geodatabase.
|
||||||
|
serviceFeatureTable?.applyEdits()?.onSuccess {
|
||||||
|
snackBarMessage = "Moved feature ${selectedFeature.attributes["objectid"]}"
|
||||||
|
}?.onFailure {
|
||||||
|
snackBarMessage =
|
||||||
|
"Failed to move feature ${selectedFeature.attributes["objectid"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun zoomToFeature(objectId: Int){
|
||||||
|
viewModelScope.launch {
|
||||||
|
val queryParameters = QueryParameters().apply {
|
||||||
|
whereClause = "OBJECTID = ${objectId}"
|
||||||
|
}
|
||||||
|
serviceFeatureTable.queryFeatures(queryParameters).onSuccess {
|
||||||
|
featureResult ->
|
||||||
|
val feature = featureResult.firstOrNull() as? ArcGISFeature
|
||||||
|
feature?.let{ feature->
|
||||||
|
feature.geometry?.let{ geometry ->
|
||||||
|
mapViewProxy.setViewpointGeometry(geometry,50.0)
|
||||||
|
}
|
||||||
|
featureLayer.clearSelection()
|
||||||
|
featureLayer.selectFeature(feature)
|
||||||
|
selectedFeature = feature
|
||||||
|
}
|
||||||
|
}.onFailure { error ->
|
||||||
|
snackBarMessage = "Feature nicht gefunden!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class FeatureOperationType(val operationName: String, val instruction: String) {
|
||||||
|
CREATE("Create feature", "Tap on the map to create a new feature."),
|
||||||
|
DELETE("Delete feature", "Select an existing feature to delete it."),
|
||||||
|
UPDATE_ATTRIBUTE("Update attribute", "Select an existing feature to edit its attribute."),
|
||||||
|
UPDATE_GEOMETRY("Update geometry", "Select an existing feature and tap the map to move it to a new position.")
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
|
||||||
|
|
||||||
|
/* Copyright 2025 Esri
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
|
package de.jadehs.strassenschadenpro2.components
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MenuAnchorType
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable component to simplify the usage of an [ExposedDropdownMenuBox].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DropDownMenuBox(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
textFieldValue: String,
|
||||||
|
textFieldLabel: String,
|
||||||
|
dropDownItemList: List<String>,
|
||||||
|
onIndexSelected: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
modifier = modifier,
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { expanded = !expanded }
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
label = { Text(textFieldLabel) },
|
||||||
|
value = textFieldValue,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
|
modifier = Modifier.menuAnchor(type = MenuAnchorType.PrimaryNotEditable)
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
dropDownItemList.forEachIndexed { index, scalebarStyle ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(scalebarStyle) },
|
||||||
|
onClick = {
|
||||||
|
onIndexSelected(index)
|
||||||
|
expanded = false
|
||||||
|
})
|
||||||
|
// Show a divider between dropdown menu options
|
||||||
|
if (index < dropDownItemList.lastIndex) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package de.jadehs.strassenschadenpro2.components.album
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User generated events that can be triggered from the UI.
|
||||||
|
*/
|
||||||
|
sealed class Intent {
|
||||||
|
data class OnPermissionGrantedWith(val compositionContext: Context): Intent()
|
||||||
|
data object OnPermissionDenied: Intent()
|
||||||
|
data class OnImageSavedWith (val compositionContext: Context): Intent()
|
||||||
|
data object OnImageSavingCanceled: Intent()
|
||||||
|
data class OnFinishPickingImagesWith(val compositionContext: Context, val imageUrls: List<Uri>): Intent()
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package de.jadehs.strassenschadenpro2.components.album
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
|
||||||
|
data class AlbumViewState(
|
||||||
|
/**
|
||||||
|
* holds the URL of the temporary file which stores the image taken by the camera.
|
||||||
|
*/
|
||||||
|
val tempFileUrl: Uri? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* holds the list of images taken by camera or selected pictures from the gallery.
|
||||||
|
*/
|
||||||
|
val selectedPictures: List<ImageBitmap> = emptyList(),
|
||||||
|
|
||||||
|
val imageUris: List<Uri> = emptyList()
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package de.jadehs.strassenschadenpro2.components.album
|
||||||
|
|
||||||
|
data class PhotoData(
|
||||||
|
val name: String,
|
||||||
|
val contentType: String,
|
||||||
|
val data: ByteArray)
|
||||||
@@ -1,10 +1,304 @@
|
|||||||
package de.jadehs.strassenschadenpro2.pages
|
package de.jadehs.strassenschadenpro2.pages
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
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.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
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.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.arcgismaps.geometry.Point
|
||||||
|
import com.arcgismaps.geometry.SpatialReference
|
||||||
|
import com.arcgismaps.location.LocationDisplayAutoPanMode
|
||||||
|
import com.arcgismaps.toolkit.geoviewcompose.MapView
|
||||||
|
import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
|
||||||
|
import com.google.android.gms.location.LocationServices
|
||||||
|
import de.jadehs.strassenschadenpro2.AlbumViewModel
|
||||||
|
import de.jadehs.strassenschadenpro2.MapViewModel
|
||||||
|
import de.jadehs.strassenschadenpro2.components.DropDownMenuBox
|
||||||
|
import de.jadehs.strassenschadenpro2.components.album.AlbumViewState
|
||||||
|
import de.jadehs.strassenschadenpro2.components.album.Intent
|
||||||
|
import de.jadehs.strassenschadenpro2.components.album.PhotoData
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CreatePage(modifier: Modifier = Modifier, mapViewModel: MapViewModel) {
|
||||||
|
var beschreibungTextFieldValue = remember { mutableStateOf(TextFieldValue("")) }
|
||||||
|
|
||||||
|
var currentDamageType = remember { mutableStateOf("")}
|
||||||
|
|
||||||
|
var showModalBottomSheet = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var enableSaveButton = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var albumViewModel = remember { AlbumViewModel(coroutineScope.coroutineContext) }
|
||||||
|
|
||||||
|
val locationDisplay = rememberLocationDisplay().apply {
|
||||||
|
setAutoPanMode(LocationDisplayAutoPanMode.Off)
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
if (checkPermissions(context)) {
|
||||||
|
// Permissions are already granted.
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
locationDisplay.dataSource.start()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RequestPermissions(
|
||||||
|
context = context,
|
||||||
|
onPermissionsGranted = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
locationDisplay.dataSource.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val fuesedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
|
||||||
|
|
||||||
|
Column(modifier = modifier.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = beschreibungTextFieldValue.value,
|
||||||
|
onValueChange = {
|
||||||
|
beschreibungTextFieldValue.value = it
|
||||||
|
enableSaveButton.value = beschreibungTextFieldValue.value.text.isNotEmpty() && currentDamageType.value.isNotEmpty()
|
||||||
|
},
|
||||||
|
minLines = 5,
|
||||||
|
label = { Text("Beschreibung")}
|
||||||
|
)
|
||||||
|
DropDownMenuBox(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp),
|
||||||
|
textFieldLabel = "Select damage type",
|
||||||
|
textFieldValue = currentDamageType.value,
|
||||||
|
dropDownItemList = mapViewModel.damageTypeList,
|
||||||
|
onIndexSelected = { index ->
|
||||||
|
currentDamageType.value = mapViewModel.damageTypeList[index]
|
||||||
|
enableSaveButton.value = beschreibungTextFieldValue.value.text.isNotEmpty() && currentDamageType.value.isNotEmpty()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
AlbumScreen(modifier= Modifier, viewModel = albumViewModel)
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly){
|
||||||
|
Button(enabled = enableSaveButton.value,
|
||||||
|
onClick = {
|
||||||
|
fuesedLocationClient.lastLocation.addOnSuccessListener { location ->
|
||||||
|
location?.let { location->
|
||||||
|
val beschreibung = beschreibungTextFieldValue.value.text
|
||||||
|
val typ = currentDamageType.value
|
||||||
|
var point = Point(location.longitude,
|
||||||
|
location.latitude,
|
||||||
|
SpatialReference.wgs84())
|
||||||
|
val photoDataList = getPhotosFromAlbum(context,albumViewModel)
|
||||||
|
mapViewModel.createFeatureWithAttributesAtPoint(
|
||||||
|
point,
|
||||||
|
beschreibung,
|
||||||
|
typ,
|
||||||
|
photoDataList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}){
|
||||||
|
Text("GPS")
|
||||||
|
}
|
||||||
|
Button(enabled = enableSaveButton.value,
|
||||||
|
onClick = {
|
||||||
|
showModalBottomSheet.value = !showModalBottomSheet.value
|
||||||
|
}){
|
||||||
|
Text("Karte")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(showModalBottomSheet.value) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
sheetGesturesEnabled = false,
|
||||||
|
onDismissRequest = {
|
||||||
|
showModalBottomSheet.value = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
MapView(
|
||||||
|
modifier = Modifier,
|
||||||
|
arcGISMap = mapViewModel.map,
|
||||||
|
locationDisplay = locationDisplay,
|
||||||
|
mapViewProxy = mapViewModel.mapViewProxy,
|
||||||
|
onSingleTapConfirmed = { singleTapConfirmedEvent ->
|
||||||
|
val screenCoordinate = singleTapConfirmedEvent.screenCoordinate
|
||||||
|
val beschreibung = beschreibungTextFieldValue.value.text
|
||||||
|
val typ = currentDamageType.value
|
||||||
|
val photoDataList=getPhotosFromAlbum(context,albumViewModel)
|
||||||
|
mapViewModel.createFeatureWithAttributesAt(
|
||||||
|
screenCoordinate,
|
||||||
|
beschreibung,
|
||||||
|
typ,
|
||||||
|
photoDataList
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CreatePage(modifier: Modifier = Modifier) {
|
fun AlbumScreen(modifier: Modifier = Modifier, viewModel: AlbumViewModel) {
|
||||||
Text("Erstellen")
|
// collecting the flow from the view model as a state allows our ViewModel and View
|
||||||
}
|
// to be in sync with each other.
|
||||||
|
val viewState: AlbumViewState by viewModel.viewStateFlow.collectAsState()
|
||||||
|
|
||||||
|
val currentContext = LocalContext.current
|
||||||
|
|
||||||
|
// launches photo picker
|
||||||
|
val pickImageFromAlbumLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { urls ->
|
||||||
|
viewModel.onReceive(Intent.OnFinishPickingImagesWith(currentContext, urls))
|
||||||
|
}
|
||||||
|
|
||||||
|
// launches camera
|
||||||
|
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { isImageSaved ->
|
||||||
|
if (isImageSaved) {
|
||||||
|
viewModel.onReceive(Intent.OnImageSavedWith(currentContext))
|
||||||
|
} else {
|
||||||
|
// handle image saving error or cancellation
|
||||||
|
viewModel.onReceive(Intent.OnImageSavingCanceled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// launches camera permissions
|
||||||
|
val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted ->
|
||||||
|
if (permissionGranted) {
|
||||||
|
viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
|
||||||
|
} else {
|
||||||
|
// handle permission denied such as:
|
||||||
|
viewModel.onReceive(Intent.OnPermissionDenied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this ensures that the camera is launched only once when the url of the temp file changes
|
||||||
|
LaunchedEffect(key1 = viewState.tempFileUrl) {
|
||||||
|
viewState.tempFileUrl?.let {
|
||||||
|
cameraLauncher.launch(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// basic view that has 2 buttons and a grid for selected pictures
|
||||||
|
Column(modifier = Modifier.padding(20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Row {
|
||||||
|
Button(onClick = {
|
||||||
|
// get user's permission first to use camera
|
||||||
|
permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
}) {
|
||||||
|
Text(text = "Take a photo")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Button(onClick = {
|
||||||
|
// Image picker does not require special permissions and can be activated right away
|
||||||
|
val mediaRequest = PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
pickImageFromAlbumLauncher.launch(mediaRequest)
|
||||||
|
}) {
|
||||||
|
Text(text = "Pick a picture")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(text = "Selected Pictures")
|
||||||
|
LazyVerticalGrid(modifier = Modifier.fillMaxWidth().heightIn(0.dp, 1200.dp),
|
||||||
|
columns = GridCells.Adaptive(150.dp),
|
||||||
|
userScrollEnabled = false) {
|
||||||
|
itemsIndexed(viewState.selectedPictures) { index, picture ->
|
||||||
|
Image(modifier = Modifier.padding(8.dp),
|
||||||
|
bitmap = picture,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPhotosFromAlbum(context: Context,albumViewModel: AlbumViewModel): List<PhotoData>{
|
||||||
|
var photoDataList = mutableListOf<PhotoData>()
|
||||||
|
|
||||||
|
albumViewModel.viewStateFlow.value.imageUris.forEach { uri ->
|
||||||
|
context.contentResolver.openInputStream(uri).use { inputStream ->
|
||||||
|
try {
|
||||||
|
val name = "image_${System.currentTimeMillis()}.png"
|
||||||
|
val contentType = "image/png"
|
||||||
|
val byteArray = inputStream?.readBytes()
|
||||||
|
|
||||||
|
if (byteArray!=null) {
|
||||||
|
photoDataList.add(
|
||||||
|
PhotoData(
|
||||||
|
name,
|
||||||
|
contentType,
|
||||||
|
byteArray
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}catch (e: Exception){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return photoDataList
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,77 @@
|
|||||||
package de.jadehs.strassenschadenpro2.pages
|
package de.jadehs.strassenschadenpro2.pages
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.jadehs.strassenschadenpro2.Feature
|
||||||
|
import de.jadehs.strassenschadenpro2.ListViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListPage(modifier: Modifier = Modifier) {
|
fun ListPage(modifier: Modifier = Modifier, listViewModel: ListViewModel, onFeatureClick: (Int) -> Unit) {
|
||||||
Text("Liste")
|
val features = listViewModel.features.collectAsState()
|
||||||
}
|
LaunchedEffect(Unit) {
|
||||||
|
listViewModel.fetch()
|
||||||
|
Log.d("test",""+features.value.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(modifier = modifier) {
|
||||||
|
items(features.value){ feature ->
|
||||||
|
FeatureItem(feature = feature,
|
||||||
|
onClick = {
|
||||||
|
feature.properties?.OBJECTID?.let { objectId ->
|
||||||
|
onFeatureClick(objectId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FeatureItem(feature: Feature, onClick: () -> Unit){
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {onClick()}
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Typ: ${feature.properties?.Typ ?: "-"}",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Beschreibung: ${feature.properties?.Beschreibung ?: "-"}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
val coords = feature.geometry?.coordinates
|
||||||
|
val coordsText = if (coords != null && coords.size == 2) "${coords[0]}, ${coords[1]}" else "-"
|
||||||
|
Text(
|
||||||
|
text = "Koordinaten: ${coordsText}",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.padding(top= 12.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,188 @@
|
|||||||
package de.jadehs.strassenschadenpro2.pages
|
package de.jadehs.strassenschadenpro2.pages
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.arcgismaps.ApiKey
|
||||||
|
import com.arcgismaps.ArcGISEnvironment
|
||||||
|
import com.arcgismaps.location.LocationDisplayAutoPanMode
|
||||||
|
import com.arcgismaps.mapping.ArcGISMap
|
||||||
|
import com.arcgismaps.mapping.BasemapStyle
|
||||||
|
import com.arcgismaps.mapping.Viewpoint
|
||||||
|
import com.arcgismaps.toolkit.geoviewcompose.MapView
|
||||||
|
import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
|
||||||
|
import de.jadehs.strassenschadenpro2.BuildConfig
|
||||||
|
import de.jadehs.strassenschadenpro2.FeatureOperationType
|
||||||
|
import de.jadehs.strassenschadenpro2.MapViewModel
|
||||||
|
import de.jadehs.strassenschadenpro2.components.DropDownMenuBox
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MapPage(modifier: Modifier = Modifier) {
|
fun MapPage(modifier: Modifier = Modifier,
|
||||||
Text("Karte")
|
mapViewModel: MapViewModel,
|
||||||
|
selectedObjectId: Int? = null,
|
||||||
|
onObjectIdUsed: () -> Unit = {}) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var featureManagementDropdownIndex by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
ArcGISEnvironment.applicationContext = context.applicationContext
|
||||||
|
ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ARCGIS_TOKEN)
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
val locationDisplay = rememberLocationDisplay().apply {
|
||||||
|
setAutoPanMode(LocationDisplayAutoPanMode.Off)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(selectedObjectId) {
|
||||||
|
selectedObjectId?.let{
|
||||||
|
mapViewModel.zoomToFeature(selectedObjectId)
|
||||||
|
onObjectIdUsed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkPermissions(context)) {
|
||||||
|
// Permissions are already granted.
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
locationDisplay.dataSource.start()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RequestPermissions(
|
||||||
|
context = context,
|
||||||
|
onPermissionsGranted = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
locationDisplay.dataSource.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
MapView(
|
||||||
|
modifier = Modifier.weight(90f),
|
||||||
|
arcGISMap = mapViewModel.map,
|
||||||
|
locationDisplay = locationDisplay,
|
||||||
|
mapViewProxy = mapViewModel.mapViewProxy,
|
||||||
|
onSingleTapConfirmed = mapViewModel::onTap,
|
||||||
|
) {
|
||||||
|
mapViewModel.selectedFeature?.let { selectedFeature ->
|
||||||
|
// Only show the delete button when on the delete feature operation and a feature is selected.
|
||||||
|
if (mapViewModel.currentFeatureOperation == FeatureOperationType.DELETE) {
|
||||||
|
Callout(geoElement = selectedFeature) {
|
||||||
|
Button(onClick = mapViewModel::deleteSelectedFeature) {
|
||||||
|
Text(text = "Delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only show the dropdown for damage type when on the update feature operation.
|
||||||
|
if (mapViewModel.currentFeatureOperation == FeatureOperationType.UPDATE_ATTRIBUTE) {
|
||||||
|
Callout(geoElement = selectedFeature) {
|
||||||
|
DropDownMenuBox(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp),
|
||||||
|
textFieldLabel = "Select damage type",
|
||||||
|
textFieldValue = mapViewModel.currentDamageType,
|
||||||
|
dropDownItemList = mapViewModel.damageTypeList,
|
||||||
|
onIndexSelected = { index ->
|
||||||
|
if (mapViewModel.selectedFeature != null) {
|
||||||
|
mapViewModel.onDamageTypeSelected(index)
|
||||||
|
} else {
|
||||||
|
coroutineScope.launch {
|
||||||
|
snackbarHostState.showSnackbar("Please select a feature to update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropDownMenuBox(
|
||||||
|
modifier = Modifier.weight(10f).padding(end = 8.dp),
|
||||||
|
textFieldLabel = "Feature management operation",
|
||||||
|
textFieldValue = mapViewModel.currentFeatureOperation.operationName,
|
||||||
|
dropDownItemList = FeatureOperationType.entries.map { entry -> entry.operationName },
|
||||||
|
onIndexSelected = { index ->
|
||||||
|
mapViewModel.onFeatureOperationSelected(index)
|
||||||
|
featureManagementDropdownIndex = index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkPermissions(context: Context): Boolean {
|
||||||
|
// Check permissions to see if both permissions are granted.
|
||||||
|
// Coarse location permission.
|
||||||
|
val permissionCheckCoarseLocation = ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
// Fine location permission.
|
||||||
|
val permissionCheckFineLocation = ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
return permissionCheckCoarseLocation && permissionCheckFineLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showError(context: Context, message: String) {
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RequestPermissions(context: Context, onPermissionsGranted: () -> Unit) {
|
||||||
|
|
||||||
|
// Create an activity result launcher using permissions contract and handle the result.
|
||||||
|
val activityResultLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { permissions ->
|
||||||
|
// Check if both fine & coarse location permissions are true.
|
||||||
|
if (permissions.all { it.value }) {
|
||||||
|
onPermissionsGranted()
|
||||||
|
} else {
|
||||||
|
showError(context, "Location permissions were denied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
activityResultLauncher.launch(
|
||||||
|
// Request both fine and coarse location permissions.
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
5
app/src/main/res/xml/file_paths.xml
Normal file
5
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- creates a reference to the cache folder that the system maintains -->
|
||||||
|
<cache-path name="temporary_camera_images" path="/" />
|
||||||
|
</paths>
|
||||||
@@ -7,7 +7,8 @@ junitVersion = "1.3.0"
|
|||||||
espressoCore = "3.7.0"
|
espressoCore = "3.7.0"
|
||||||
lifecycleRuntimeKtx = "2.9.4"
|
lifecycleRuntimeKtx = "2.9.4"
|
||||||
activityCompose = "1.11.0"
|
activityCompose = "1.11.0"
|
||||||
composeBom = "2024.09.00"
|
composeBom = "2025.12.00"
|
||||||
|
arcgisMapsKotlin = "200.8.0"
|
||||||
|
|
||||||
[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" }
|
||||||
@@ -24,9 +25,15 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
|
|||||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
arcgis-maps-kotlin = { group = "com.esri", name = "arcgis-maps-kotlin", 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-authentication = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-authentication" }
|
||||||
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
kotlin-serialization = { id="org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ dependencyResolutionManagement {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url = uri("https://esri.jfrog.io/artifactory/arcgis") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user