Compare commits

11 Commits
main ... main

Author SHA1 Message Date
a5f8746fc4 Toom to selected Feature 2025-12-18 11:10:48 +01:00
1c5d43bb02 Auflistung der Features 2025-12-18 10:14:16 +01:00
0194aabe62 Json laden 2025-12-18 09:30:46 +01:00
0c045c8c91 Attachments hinzufügen 2025-12-11 10:43:06 +01:00
bd66f298e0 Add GPS and map-based feature creation to CreatePage with attribute validation, MapViewModel enhancements for feature handling, and dependency updates. 2025-12-04 11:02:45 +01:00
56c61d38a1 Add CreatePage with text input, dropdown, modal map, and location services; refactor MapPage and ContentScreen for MapViewModel injection 2025-12-04 09:26:19 +01:00
549cf73ede Add feature management functionality to MapPage and MapViewModel with feature creation, deletion, attribute updates, geometry updates, and dropdown UI component integration. 2025-11-27 11:17:14 +01:00
3c1c91b5b7 Add location services to MapPage with permission handling and LocationDisplay integration 2025-11-20 11:06:08 +01:00
78b5e0264e Integrate FeatureLayer with ServiceFeatureTable into MapViewModel and set initial viewpoint for ArcGISMap. 2025-11-20 10:39:03 +01:00
04fd485cf9 Pass Application context to MapPage and ContentScreen, create MapViewModel for ArcGISMap management, and refactor MapPage to display an interactive map. 2025-11-20 09:32:22 +01:00
c05ea2878e Integrate ArcGIS SDK with token and dependencies, enable buildConfig, and add Internet permission 2025-11-20 08:46:26 +01:00
17 changed files with 1337 additions and 17 deletions

View File

@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
}
android {
@@ -18,6 +19,14 @@ android {
versionName = "1.0"
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 {
@@ -38,6 +47,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}
@@ -57,4 +67,13 @@ dependencies {
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
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)
}

View File

@@ -22,6 +22,22 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</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>
<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>

View File

@@ -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
}

View 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
)

View File

@@ -34,9 +34,10 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
StrassenSchadenPro2Theme {
MainScreen()
MainScreen(application=application)
}
}
}

View File

@@ -1,6 +1,6 @@
package de.jadehs.strassenschadenpro2
import android.util.Log
import android.app.Application
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
@@ -25,7 +25,7 @@ import de.jadehs.strassenschadenpro2.pages.MapPage
import de.jadehs.strassenschadenpro2.pages.SettingsPage
@Composable
fun MainScreen(modifier: Modifier = Modifier) {
fun MainScreen(modifier: Modifier = Modifier, application: Application) {
val navItemList = listOf(
NavItem("Karte",Icons.Default.Place),
@@ -36,6 +36,11 @@ fun MainScreen(modifier: Modifier = Modifier) {
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(),
bottomBar = {
NavigationBar {
@@ -52,17 +57,36 @@ fun MainScreen(modifier: Modifier = Modifier) {
}
}) {
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
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) {
0 -> MapPage()
1 -> CreatePage()
2 -> ListPage()
0 -> MapPage(modifier = modifier, mapViewModel = mapViewModel,selectedObjectId=selectedObjectId, onObjectIdUsed = onObjectIdUsed)
1 -> CreatePage(modifier = modifier, mapViewModel = mapViewModel)
2 -> ListPage(modifier = modifier, listViewModel = listViewModel, onFeatureClick = onFeatureSelected)
3 -> SettingsPage()
}
}

View 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.")
}

View File

@@ -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()
}
}
}
}
}

View File

@@ -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()
}

View File

@@ -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()
)

View File

@@ -0,0 +1,6 @@
package de.jadehs.strassenschadenpro2.components.album
data class PhotoData(
val name: String,
val contentType: String,
val data: ByteArray)

View File

@@ -1,10 +1,304 @@
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.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.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
fun CreatePage(modifier: Modifier = Modifier) {
Text("Erstellen")
fun AlbumScreen(modifier: Modifier = Modifier, viewModel: AlbumViewModel) {
// 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
}

View File

@@ -1,10 +1,77 @@
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.jadehs.strassenschadenpro2.Feature
import de.jadehs.strassenschadenpro2.ListViewModel
@Composable
fun ListPage(modifier: Modifier = Modifier) {
Text("Liste")
fun ListPage(modifier: Modifier = Modifier, listViewModel: ListViewModel, onFeatureClick: (Int) -> Unit) {
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))
}
}

View File

@@ -1,10 +1,188 @@
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.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.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
fun MapPage(modifier: Modifier = Modifier) {
Text("Karte")
fun MapPage(modifier: Modifier = Modifier,
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
)
)
}
}

View 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>

View File

@@ -7,7 +7,8 @@ junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.4"
activityCompose = "1.11.0"
composeBom = "2024.09.00"
composeBom = "2025.12.00"
arcgisMapsKotlin = "200.8.0"
[libraries]
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-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
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]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", 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"}

View File

@@ -16,6 +16,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://esri.jfrog.io/artifactory/arcgis") }
}
}