From 88b1bf96a00d37b3bca6518c7125c11a2d1462aa Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Fri, 22 Nov 2024 15:16:28 +0000 Subject: [PATCH 01/13] Migrate "Add Features with Contingent Values" to Compose --- .../README.md | 3 +- .../README.metadata.json | 84 ++-- .../build.gradle.kts | 1 + .../src/main/AndroidManifest.xml | 5 - .../MainActivity.kt | 450 +----------------- .../components/MapViewModel.kt | 343 +++++++++++++ .../screens/MainScreen.kt | 272 +++++++++++ 7 files changed, 682 insertions(+), 476 deletions(-) create mode 100644 samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt create mode 100644 samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt diff --git a/samples/add-features-with-contingent-values/README.md b/samples/add-features-with-contingent-values/README.md index 4a7c828e0..52c2d4885 100644 --- a/samples/add-features-with-contingent-values/README.md +++ b/samples/add-features-with-contingent-values/README.md @@ -48,8 +48,9 @@ The mobile geodatabase contains birds nests in the Fillmore area, defined with c ## Additional information +This sample uses the GeoCompose Toolkit module to be able to implement a Composable MapView. Learn more about contingent values and how to utilize them on the [ArcGIS Pro documentation](https://pro.arcgis.com/en/pro-app/latest/help/data/geodatabases/overview/contingent-values.htm). ## Tags -coded values, contingent values, feature table, geodatabase +coded values, contingent values, feature table, geodatabase, geoviewcompose, toolkit diff --git a/samples/add-features-with-contingent-values/README.metadata.json b/samples/add-features-with-contingent-values/README.metadata.json index 0c87a91c4..c7c889cd4 100644 --- a/samples/add-features-with-contingent-values/README.metadata.json +++ b/samples/add-features-with-contingent-values/README.metadata.json @@ -1,42 +1,46 @@ { - "category": "Edit and Manage Data", - "description": "Create and add features whose attribute values satisfy a predefined set of contingencies.", - "formal_name": "AddFeaturesWithContingentValues", - "ignore": false, - "images": [ - "add-features-with-contingent-values.png" - ], - "keywords": [ - "coded values", - "contingent values", - "feature table", - "geodatabase", - "ArcGISFeatureTable", - "CodedValue", - "CodedValueDomain", - "ContingencyConstraintViolation", - "ContingentCodedValue", - "ContingentRangeValue", - "ContingentValuesDefinition", - "ContingentValuesResult" - ], - "language": "kotlin", - "redirect_from": [ - "/android/latest/sample-code/add-features-with-contingent-values.htm" - ], - "relevant_apis": [ - "ArcGISFeatureTable", - "CodedValue", - "CodedValueDomain", - "ContingencyConstraintViolation", - "ContingentCodedValue", - "ContingentRangeValue", - "ContingentValuesDefinition", - "ContingentValuesResult" - ], - "snippets": [ - "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/MainActivity.kt", - "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/DownloadActivity.kt" - ], - "title": "Add features with contingent values" + "category": "Edit and Manage Data", + "description": "Create and add features whose attribute values satisfy a predefined set of contingencies.", + "formal_name": "AddFeaturesWithContingentValues", + "ignore": false, + "images": [ + "add-features-with-contingent-values.png" + ], + "keywords": [ + "coded values", + "contingent values", + "feature table", + "geodatabase", + "geoviewcompose", + "toolkit", + "ArcGISFeatureTable", + "CodedValue", + "CodedValueDomain", + "ContingencyConstraintViolation", + "ContingentCodedValue", + "ContingentRangeValue", + "ContingentValuesDefinition", + "ContingentValuesResult" + ], + "language": "kotlin", + "redirect_from": [ + "/android/latest/sample-code/add-features-with-contingent-values.htm" + ], + "relevant_apis": [ + "ArcGISFeatureTable", + "CodedValue", + "CodedValueDomain", + "ContingencyConstraintViolation", + "ContingentCodedValue", + "ContingentRangeValue", + "ContingentValuesDefinition", + "ContingentValuesResult" + ], + "snippets": [ + "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt", + "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/DownloadActivity.kt", + "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/MainActivity.kt", + "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt" + ], + "title": "Add features with contingent values" } diff --git a/samples/add-features-with-contingent-values/build.gradle.kts b/samples/add-features-with-contingent-values/build.gradle.kts index f0063b919..b957faf49 100644 --- a/samples/add-features-with-contingent-values/build.gradle.kts +++ b/samples/add-features-with-contingent-values/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.arcgismaps.android.library) + alias(libs.plugins.arcgismaps.android.library.compose) alias(libs.plugins.arcgismaps.kotlin.sample) alias(libs.plugins.gradle.secrets) } diff --git a/samples/add-features-with-contingent-values/src/main/AndroidManifest.xml b/samples/add-features-with-contingent-values/src/main/AndroidManifest.xml index d06dffa1e..011353d26 100644 --- a/samples/add-features-with-contingent-values/src/main/AndroidManifest.xml +++ b/samples/add-features-with-contingent-values/src/main/AndroidManifest.xml @@ -7,11 +7,6 @@ android:name=".DownloadActivity" android:exported="true" android:label="@string/add_features_with_contingent_values_app_name"> - - - createBottomSheet(mapPoint) } + setContent { + SampleAppTheme { + SampleApp() } } - - // create a temporary directory to use the geodatabase file - createGeodatabaseCacheDirectory() - - lifecycleScope.launch { - // retrieve and load the offline mobile geodatabase file from the cache directory - geodatabase.load().getOrElse { - showError("Error loading GeoDatabase: ${it.message}") - } - - // get the first geodatabase feature table - val featureTable = geodatabase.featureTables.firstOrNull() - ?: return@launch showError("No feature table found in geodatabase") - // load the geodatabase feature table - featureTable.load().getOrElse { - return@launch showError(it.message.toString()) - } - - // create and load the feature layer from the feature table - val featureLayer = FeatureLayer.createWithFeatureTable(featureTable) - // add the feature layer to the map - mapView.map?.operationalLayers?.add(featureLayer) - - // set the map's viewpoint to the feature layer's full extent - val extent = featureLayer.fullExtent - ?: return@launch showError("Error retrieving extent of the feature layer") - mapView.setViewpoint(Viewpoint(extent)) - - // keep the instance of the featureTable - this@MainActivity.featureTable = featureTable - - // add buffer graphics for the feature layer - queryFeatures() - } } - /** - * Geodatabase creates and uses various temporary files while processing a database, - * which will need to be cleared before looking up the [geodatabase] again. - * A copy of the original geodatabase file is created in the cache folder. - */ - private fun createGeodatabaseCacheDirectory() { - // clear cache directory - File(cacheDir.path).deleteRecursively() - // copy over the original Geodatabase file to be used in the temp cache directory - File("$provisionPath/ContingentValuesBirdNests.geodatabase").copyTo( - File("${cacheDir.path}/ContingentValuesBirdNests.geodatabase") - ) - } - - /** - * Create buffer graphics for the features and adds the graphics to - * the [graphicsOverlay] - */ - private suspend fun queryFeatures() { - // clear the existing graphics - graphicsOverlay.graphics.clear() - - // create buffer graphics for the features - val queryParameters = QueryParameters().apply { - // set the where clause to filter for buffer sizes greater than 0 - whereClause = "BufferSize > 0" + @Composable + private fun SampleApp() { + Surface( + color = MaterialTheme.colorScheme.background + ) { + MainScreen( + sampleName = getString(R.string.add_features_with_contingent_values_app_name) + ) } - - // query the features using the queryParameters on the featureTable - val featureQueryResult = featureTable?.queryFeatures(queryParameters)?.getOrThrow() - // call get on the future to get the result - val featureResultList = featureQueryResult?.toList() - - if (!featureResultList.isNullOrEmpty()) { - // create list of graphics for each query result - val graphics = featureResultList.map { createGraphic(it) } - // add the graphics to the graphics overlay - graphicsOverlay.graphics.addAll(graphics) - } else { - showError("No features found with BufferSize > 0") - } - } - - /** - * Create a graphic for the given [feature] and returns a Graphic with the features attributes - */ - private fun createGraphic(feature: Feature): Graphic { - // get the feature's buffer size - val bufferSize = feature.attributes["BufferSize"] as Int - // get a polygon using the feature's buffer size and geometry - val polygon = feature.geometry?.let { GeometryEngine.bufferOrNull(it, bufferSize.toDouble()) } - // create the outline for the buffers - val lineSymbol = SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.black, 2f) - // create the buffer symbol - val bufferSymbol = SimpleFillSymbol( - SimpleFillSymbolStyle.ForwardDiagonal, Color.red, lineSymbol - ) - // create an graphic using the geometry and fill symbol - return Graphic(polygon, bufferSymbol) - } - - /** - * Creates a BottomSheetDialog view to handle contingent value interaction. - * Once the contingent values have been set and the apply button is clicked, - * the function will call validateContingency() to add the feature at the [mapPoint]. - */ - private fun createBottomSheet(mapPoint: Point) { - // creates a new BottomSheetDialog - val bottomSheet = BottomSheetDialog(this).apply { - behavior.state = BottomSheetBehavior.STATE_EXPANDED - } - - // set up the first content value attribute - setUpStatusAttributes() - - // clear and set bottom sheet content view to layout, - // to be able to set the content view on each bottom sheet draw - if (bottomSheetBinding.root.parent != null) { - (bottomSheetBinding.root.parent as ViewGroup).removeAllViews() - } - - // reset feature to null since this is a new feature - feature = null - - bottomSheetBinding.apply { - - // reset bottom sheet values, this is needed to showcase contingent values behavior - statusInputLayout.editText?.setText("") - protectionInputLayout.editText?.setText("") - selectedBuffer.text = "" - protectionInputLayout.isEnabled = false - bufferSeekBar.isEnabled = false - bufferSeekBar.value = bufferSeekBar.valueFrom - - // set apply button to validate and apply contingency feature on map - applyTv.setOnClickListener { - // check if the contingent features set is valid and set it to the map if valid - validateContingency(mapPoint) - bottomSheet.dismiss() - } - - // dismiss on cancel clicked - cancelTv.setOnClickListener { bottomSheet.dismiss() } - } - - // set the content view to the root of the binding layout - bottomSheet.setContentView(bottomSheetBinding.root) - // display the bottom sheet view - bottomSheet.show() - } - - /** - * Retrieve the status fields, add the fields to a ContingentValueDomain, and set the values to the spinner - * When status attribute selected, createFeature() is called. - */ - private fun setUpStatusAttributes() { - // get the first field by name - val statusField = featureTable?.fields?.find { field -> field.name == "Status" } - // get the field's domains as coded value domain - val codedValueDomain = statusField?.domain as CodedValueDomain - // get the coded value domain's coded values - val statusCodedValues = codedValueDomain.codedValues - // get the selected index if applicable - val statusNames = statusCodedValues.map { it.name } - // get the items to be added to the spinner adapter - val adapter = ArrayAdapter(bottomSheetBinding.root.context, R.layout.list_item, statusNames) - (bottomSheetBinding.statusInputLayout.editText as AutoCompleteTextView).apply { - setAdapter(adapter) - setOnItemClickListener { _, _, position, _ -> - // get the CodedValue of the item selected, and create a feature needed for feature attributes - createFeature(statusCodedValues[position]) - } - } - } - - /** - * Set up the [feature] using the status attribute's coded value - * by loading the [featureTable]'s Contingent Value Definition. - * This function calls setUpProtectionAttributes() once the [feature] has been set - */ - private fun createFeature(codedValue: CodedValue) { - // get the contingent values definition from the feature table - val contingentValueDefinition = featureTable?.contingentValuesDefinition - if (contingentValueDefinition != null) { - lifecycleScope.launch { - // load the contingent values definition - contingentValueDefinition.load().getOrElse { - showError("Error loading the ContingentValuesDefinition") - } - // create a feature from the feature table and set the initial attribute - feature = featureTable?.createFeature() as ArcGISFeature - feature?.attributes?.set("Status", codedValue.code) - setUpProtectionAttributes() - } - } else { - showError("Error retrieving ContingentValuesDefinition from the FeatureTable") - } - } - - /** - * Retrieve the protection attribute fields, add the fields to a ContingentCodedValue, and set the values to the spinner - * When status attribute selected, showBufferSeekbar() is called. - */ - private fun setUpProtectionAttributes() { - // set the bottom sheet view to enable the Protection attribute, and disable input elsewhere - bottomSheetBinding.apply { - protectionInputLayout.isEnabled = true - bufferSeekBar.isEnabled = false - bufferSeekBar.value = bufferSeekBar.valueFrom - protectionInputLayout.editText?.setText("") - selectedBuffer.text = "" - } - - // get the contingent value results with the feature for the protection field - val contingentValuesResult = feature?.let { - featureTable?.getContingentValuesOrNull(it, "Protection") - } - - // get the list of contingent values by field group - val contingentValues = contingentValuesResult?.byFieldGroup?.get("ProtectionFieldGroup") - - // convert the list of ContingentValues to a list of CodedValue - val protectionCodedValues = - contingentValues?.map { (it as ContingentCodedValue).codedValue } - ?: return showError("Error getting coded values by field group") - - // get the attribute names for each coded value - val protectionNames = protectionCodedValues.map { it.name } - - // set the items to be added to the spinner adapter - val adapter = ArrayAdapter( - bottomSheetBinding.root.context, R.layout.list_item, protectionNames - ) - - // set the choices of protection attribute values - (bottomSheetBinding.protectionInputLayout.editText as AutoCompleteTextView).apply { - setAdapter(adapter) - setOnItemClickListener { _, _, position, _ -> - // set the protection CodedValue of the item selected - feature?.attributes?.set("Protection", protectionCodedValues[position].code) - // enable buffer seekbar - showBufferSeekbar() - } - } - } - - /** - * Retrieve the buffer attribute fields, add the fields - * to a ContingentRangeValue, and set the values to a SeekBar - */ - private fun showBufferSeekbar() { - // set the bottom sheet view to enable the buffer attribute - bottomSheetBinding.apply { - bufferSeekBar.isEnabled = true - selectedBuffer.text = "" - } - - // get the contingent value results using the feature and field - val contingentValueResult = feature?.let { - featureTable?.getContingentValuesOrNull(it, "BufferSize") - } - - // get the contingent rang value of the buffer size field group - val bufferSizeRangeValue = contingentValueResult?.byFieldGroup?.get("BufferSizeFieldGroup") - ?.get(0) as ContingentRangeValue - - // set the minimum and maximum possible buffer sizes - val minValue = bufferSizeRangeValue.minValue as Int - val maxValue = bufferSizeRangeValue.maxValue as Int - - // check if there can be a max value, if not disable SeekBar - // & set value to attribute size to 0 - if (maxValue > 0) { - // get SeekBar instance from the binding layout - bottomSheetBinding.bufferSeekBar.apply { - // set the min, max and current value of the SeekBar - valueFrom = minValue.toFloat() - valueTo = maxValue.toFloat() - value = valueFrom - // set the initial attribute and the text to the min of the ContingentRangeValue - feature?.attributes?.set("BufferSize", value.toInt()) - bottomSheetBinding.selectedBuffer.text = value.toInt().toString() - // set the change listener to update the attribute value and the displayed value to the SeekBar position - addOnChangeListener { _, value, _ -> - feature?.attributes?.set("BufferSize", value.toInt()) - bottomSheetBinding.selectedBuffer.text = value.toInt().toString() - } - } - } else { - // max value is 0, so disable seekbar and update the attribute value accordingly - bottomSheetBinding.apply { - bufferSeekBar.isEnabled = false - selectedBuffer.text = "0" - } - feature?.attributes?.set("BufferSize", 0) - } - } - - /** - * Ensure that the selected values are a valid combination. - * If contingencies are valid, then display [feature] on the [mapPoint] - */ - private fun validateContingency(mapPoint: Point) { - // check if all the features have been set - if (featureTable == null) { - showError("Input all values to add a feature to the map") - return - } - - // validate the feature's contingencies - val contingencyViolations = feature?.let { - featureTable?.validateContingencyConstraints(it) - } ?: return showError("No feature attribute was selected") - - // if there are no contingency violations - if (contingencyViolations.isEmpty()) { - // the feature is valid and ready to add to the feature table - // create a symbol to represent a bird's nest - val symbol = SimpleMarkerSymbol(SimpleMarkerSymbolStyle.Circle, Color.black, 11F) - // add the graphic to the graphics overlay - graphicsOverlay.graphics.add(Graphic(mapPoint, symbol)) - - // set the geometry of the feature to the map point - feature?.geometry = mapPoint - - // create the graphic of the feature - val graphic = feature?.let { createGraphic(it) } - // add the graphic to the graphics overlay - graphic?.let { graphicsOverlay.graphics.add(it) } - - // add the feature to the feature table - lifecycleScope.launch { - feature?.let { featureTable?.addFeature(it) } - feature?.load()?.getOrElse { - return@launch showError(it.message.toString()) - } - } - } else { - showError("Invalid contingent values: " + (contingencyViolations.size) + " violations found.") - } - - } - - private fun showError(message: String) { - Log.e(localClassName, message) - Snackbar.make(mapView, message, Snackbar.LENGTH_SHORT).show() } } diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt new file mode 100644 index 000000000..027434cb3 --- /dev/null +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt @@ -0,0 +1,343 @@ +/* Copyright 2024 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. + * + */ + +package com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components + +import android.app.Application +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.arcgismaps.Color +import com.arcgismaps.data.ArcGISFeature +import com.arcgismaps.data.ArcGISFeatureTable +import com.arcgismaps.data.CodedValue +import com.arcgismaps.data.CodedValueDomain +import com.arcgismaps.data.ContingentCodedValue +import com.arcgismaps.data.ContingentRangeValue +import com.arcgismaps.data.Feature +import com.arcgismaps.data.Geodatabase +import com.arcgismaps.data.QueryParameters +import com.arcgismaps.geometry.Geometry +import com.arcgismaps.geometry.GeometryEngine +import com.arcgismaps.geometry.Point +import com.arcgismaps.mapping.ArcGISMap +import com.arcgismaps.mapping.Basemap +import com.arcgismaps.mapping.Viewpoint +import com.arcgismaps.mapping.layers.ArcGISVectorTiledLayer +import com.arcgismaps.mapping.layers.FeatureLayer +import com.arcgismaps.mapping.symbology.SimpleFillSymbol +import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle +import com.arcgismaps.mapping.symbology.SimpleLineSymbol +import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle +import com.arcgismaps.mapping.symbology.SimpleMarkerSymbol +import com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyle +import com.arcgismaps.mapping.view.Graphic +import com.arcgismaps.mapping.view.GraphicsOverlay +import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.R +import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel +import java.io.File +import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class MapViewModel(application: Application) : AndroidViewModel(application) { + val cacheDir = application.cacheDir + + private val provisionPath: String by lazy { + application.getExternalFilesDir(null)?.path.toString() + + File.separator + + application.getString(R.string.add_features_with_contingent_values_app_name) + } + + // Create an empty map, to be updated once data is loaded from the feature table + var arcGISMap by mutableStateOf(ArcGISMap()) + + // Create a message dialog view model for handling error messages + val messageDialogVM = MessageDialogViewModel() + + // offline vector tiled layer to be used as a basemap + val fillmoreVectorTileLayer = ArcGISVectorTiledLayer("$provisionPath/FillmoreTopographicMap.vtpk") + + // mobile database containing offline feature data + val geodatabase: Geodatabase by lazy { + Geodatabase("${cacheDir.path}/ContingentValuesBirdNests.geodatabase") + } + + // graphics overlay used to add feature graphics to the map + val graphicsOverlay = GraphicsOverlay() + + // instance of the contingent feature to be added to the map + var feature: ArcGISFeature? = null + + // instance of the feature table retrieved from the geodatabase, updates when new feature is added + var featureTable: ArcGISFeatureTable? = null + + private val _uiState = MutableStateFlow(UIState(null, null, 0)) + val uiState = _uiState.asStateFlow() + + // state flows for possible values + private val _statusAttributes = MutableStateFlow>(listOf()) + private val _protectionAttributes = MutableStateFlow>(listOf()) + private val _sliderControlParameters = MutableStateFlow(SliderControlParameters(false, 0, 0)) + val statusAttributes = _statusAttributes.asStateFlow() + val protectionAttributes = _protectionAttributes.asStateFlow() + val sliderControlParameters = _sliderControlParameters.asStateFlow() + + init { + // create a temporary directory for use with the geodatabase file + createGeodatabaseCacheDir() + + viewModelScope.launch{ + // retrieve and load the offline mobile geodatabase file from the cache directory + geodatabase.load().getOrElse { + messageDialogVM.showMessageDialog("Error loading GeoDatabase", it.message.toString()) + } + + // get the first geodatabase feature table + val featureTable = geodatabase.featureTables.firstOrNull() ?: + return@launch messageDialogVM.showMessageDialog("Error", "Error retrieving extent of the feature layer") + + // load the geodatabase feature table + featureTable.load().getOrElse { return@launch messageDialogVM.showMessageDialog("Error", it.message.toString()) } + + // create and load the feature layer from the feature table + val featureLayer = FeatureLayer.createWithFeatureTable(featureTable) + + // get the full extent of the feature layer + val extent = featureLayer.fullExtent + + // set the basemap to the offline vector tiled layer, and viewpoint to the feature layer extent + arcGISMap = ArcGISMap(Basemap(fillmoreVectorTileLayer)).apply { + initialViewpoint = Viewpoint(boundingGeometry = extent as Geometry) + } + arcGISMap.operationalLayers.add(featureLayer) + + // keep the instance of the feature table + this@MapViewModel.featureTable = featureTable + + // add buffer graphics for the feature layer + queryExistingFeatures() + + // get status attributes for new features + _statusAttributes.value = statusAttributes() + } + + } + + /** + * Geodatabase creates and uses various temporary files while processing a database, + * which will need to be cleared before looking up the [geodatabase] again. + * A copy of the original geodatabase file is created in the cache folder. + */ + fun createGeodatabaseCacheDir(){ + // clear cache directory + File(cacheDir.path).deleteRecursively() + // copy over the original Geodatabase file to be used in the temp cache directory + File("$provisionPath/ContingentValuesBirdNests.geodatabase").copyTo( + File("${cacheDir.path}/ContingentValuesBirdNests.geodatabase") + ) + } + + /** + * Create buffer graphics for features, and adds the graphics to + * the [graphicsOverlay] + */ + private suspend fun queryExistingFeatures(){ + // clear the existing graphics + graphicsOverlay.graphics.clear() + + // create buffer graphics for features which need them + val queryParameters = QueryParameters().apply { + whereClause = "BufferSize > 0" + } + + // query the features using the queryParameters on the featureTable + val featureQueryResult = featureTable?.queryFeatures(queryParameters)?.getOrThrow() + val featureResultList = featureQueryResult?.toList() + + if (!featureResultList.isNullOrEmpty()){ + // create list of graphics for each query result + val graphics = featureResultList.map { createGraphic(it) } + // add the graphics to the graphics overlay + graphicsOverlay.graphics.addAll(graphics) + } + } + + /** + * Creates and returns a graphic using the attributes of the given [feature] + */ + fun createGraphic(feature: Feature): Graphic{ + // create the outline for the buffer symbol + val lineSymbol = SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.black, 2f) + // create the buffer symbol + val bufferSymbol = SimpleFillSymbol(SimpleFillSymbolStyle.ForwardDiagonal, Color.red, lineSymbol) + // get the feature's buffer size + val bufferSize = feature.attributes["BufferSize"] as Int + // get a polygon using the feature's buffer size and geometry + val polygon = feature.geometry?.let { GeometryEngine.bufferOrNull(it, bufferSize.toDouble()) } + + // create a graphic using the geometry and fill symbol + return Graphic(polygon, bufferSymbol) + } + + /** + * Retrieve the status fields, add the fields to a ContingentValueDomain. + * Used to display options in the UI. + */ + fun statusAttributes(): List{ + val statusField = featureTable?.fields?.find { field -> field.name == "Status" } + val codedValueDomain = statusField?.domain as CodedValueDomain + return codedValueDomain.codedValues + } + + fun onStatusAttributeSelect(codedValue: CodedValue) { + val contingentValuesDefinition = featureTable?.contingentValuesDefinition + if (contingentValuesDefinition != null){ + viewModelScope.launch{ + contingentValuesDefinition.load().getOrElse { + messageDialogVM.showMessageDialog("Error loading the contingent values definition", it.message.toString()) + } + + feature = featureTable?.createFeature() as ArcGISFeature + feature?.attributes?.set("Status", codedValue.code) + + _uiState.value = UIState(codedValue, null, 0) + + _protectionAttributes.value = protectionAttributes() + } + } else { + messageDialogVM.showMessageDialog("Error", "Error retrieving ContingentValuesDefinition from the feature table") + } + } + + fun protectionAttributes(): List { + // get the contingent value results with the feature for the protection field + val contingentValuesResult = feature?.let { + featureTable?.getContingentValuesOrNull(it, "Protection") + } + + // get the list of contingent values by field group + val contingentValues = contingentValuesResult?.byFieldGroup?.get("ProtectionFieldGroup") + + // convert the list of ContingentValues to a list of CodedValue + val protectionCodedValues : List = contingentValues?.map { (it as ContingentCodedValue).codedValue } ?: + listOf().also { messageDialogVM.showMessageDialog("Error", "Error getting coded values by field group") } + + return protectionCodedValues + } + + fun onProtectionAttributeSelect(codedValue: CodedValue){ + feature?.attributes?.set("Protection", codedValue.code) + _uiState.value = _uiState.value.copy(protection = codedValue) + _sliderControlParameters.value = bufferAttributes() + } + + fun bufferAttributes(): SliderControlParameters{ + val contingentValueResult = feature?.let { + featureTable?.getContingentValuesOrNull(it, "BufferSize") + } + + val bufferSizeRangeValue = contingentValueResult?.byFieldGroup?.get("BufferSizeFieldGroup")?.get(0) as ContingentRangeValue + val minValue = bufferSizeRangeValue.minValue as Int + val maxValue = bufferSizeRangeValue.maxValue as Int + + val sliderControlParameters = if (maxValue > 0){ + SliderControlParameters(true, minValue, maxValue) + } else { + SliderControlParameters(false, 0, 0) + } + + return sliderControlParameters + } + + fun onBufferSizeSelect(bufferSize: Int){ + feature?.attributes?.set("BufferSize", bufferSize) + _uiState.value = _uiState.value.copy(buffer=bufferSize) + } + + /** + * Ensure that the selected values are a valid combination. + * If contingencies are valid, then display [feature] on the [mapPoint] + */ + fun validateContingency(mapPoint: Point) { + // check if all the features have been set + if (featureTable == null) { + messageDialogVM.showMessageDialog("Input all values to add a feature to the map") + return + } + + // validate the feature's contingencies + val contingencyViolations = feature?.let { + featureTable?.validateContingencyConstraints(it) + } ?: return messageDialogVM.showMessageDialog("No feature attribute was selected") + + // if there are no contingency violations + if (contingencyViolations.isEmpty()) { + // the feature is valid and ready to add to the feature table + // create a symbol to represent a bird's nest + val symbol = SimpleMarkerSymbol(SimpleMarkerSymbolStyle.Circle, Color.black, 11F) + // add the graphic to the graphics overlay + graphicsOverlay.graphics.add(Graphic(mapPoint, symbol)) + + // set the geometry of the feature to the map point + feature?.geometry = mapPoint + + // create the graphic of the feature + val graphic = feature?.let { createGraphic(it) } + // add the graphic to the graphics overlay + graphic?.let { graphicsOverlay.graphics.add(it) } + + // add the feature to the feature table + viewModelScope.launch { + feature?.let { featureTable?.addFeature(it) } + feature?.load()?.getOrElse { + return@launch messageDialogVM.showMessageDialog("Error", it.message.toString()) + } + } + } else { + messageDialogVM.showMessageDialog("Error", "Invalid contingent values: " + (contingencyViolations.size) + " violations found.") + contingencyViolations.forEach { + Log.e("ContingencyViolation", it.fieldGroup.name) + } + } + + } + + /** + * Clears feature and attributes held in the view model to avoid inconsistent state + * after feature is created, fails to create, etc. + */ + fun clearFeature() { + feature = null + _uiState.value = UIState(null,null,0) + _sliderControlParameters.value = SliderControlParameters(false, 0, 0) + } + +} + +/** + * Enable status, maximum, and minimum values for the buffer size slider + */ +data class SliderControlParameters(val isEnabled: Boolean, val minRange: Int, val maxRange: Int) + +/** + * Currently selected status, protection, and buffer attributes for the feature under construction, + * used to update the UI. + */ +data class UIState(val status: CodedValue?, val protection: CodedValue?, val buffer: Int) diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt new file mode 100644 index 000000000..a3e33efec --- /dev/null +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt @@ -0,0 +1,272 @@ +/* Copyright 2024 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. + * + */ + +package com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.screens + +import androidx.compose.foundation.layout.Box +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SheetState +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arcgismaps.data.CodedValue +import com.arcgismaps.geometry.Point +import com.arcgismaps.toolkit.geoviewcompose.MapView +import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.MapViewModel +import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog +import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar +import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.SliderControlParameters +import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.UIState +import kotlin.math.roundToInt +import kotlinx.coroutines.launch + +/** + * Main screen layout for the sample app + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen(sampleName: String) { + // create a ViewModel to handle MapView interactions + val mapViewModel: MapViewModel = viewModel() + + val uiState by mapViewModel.uiState.collectAsStateWithLifecycle() + val statusAttributes by mapViewModel.statusAttributes.collectAsStateWithLifecycle() + val protectionAttributes by mapViewModel.protectionAttributes.collectAsStateWithLifecycle() + val sliderControlParameters by mapViewModel.sliderControlParameters.collectAsStateWithLifecycle() + + var mapPoint by remember { mutableStateOf(null) } + + Scaffold( + topBar = { SampleTopAppBar(title = sampleName) }, + content = { + var showBottomSheet by remember { mutableStateOf(false) } + val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + Box { + MapView( + modifier = Modifier + .fillMaxSize() + .padding(it), + arcGISMap = mapViewModel.arcGISMap, + graphicsOverlays = listOf(mapViewModel.graphicsOverlay), + onSingleTapConfirmed = { + it.mapPoint.let {point -> + mapPoint = point + showBottomSheet = true + } + } + ) + + if (showBottomSheet) { + ModalBottomSheet( + modifier = Modifier.wrapContentHeight(), + sheetState = bottomSheetState, + onDismissRequest = { + showBottomSheet = false + mapViewModel.clearFeature() + } + ) { + val onBottomSheetStateChanged = { state: SheetState -> + if (!bottomSheetState.isVisible) { + showBottomSheet = false + mapViewModel.clearFeature() + } + } + CVBSContents( + mapPoint, + bottomSheetState, + onBottomSheetStateChanged, + uiState, + statusAttributes, + mapViewModel::onStatusAttributeSelect, + protectionAttributes, + mapViewModel::onProtectionAttributeSelect, + sliderControlParameters, + mapViewModel::onBufferSizeSelect, + mapViewModel::validateContingency + ) + } + } + } + + mapViewModel.messageDialogVM.apply { + if (dialogStatus) { + MessageDialog( + title = messageTitle, + description = messageDescription, + onDismissRequest = ::dismissDialog + ) + } + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CVBSContents( + mapPoint: Point?, + bottomSheetState: SheetState, + onBottomSheetStateChange: (SheetState) -> Unit, + uiState: UIState, + statusAttributes: List, + onStatusAttributeSelect: (CodedValue) -> Unit, + protectionAttributes: List, + onProtectionAttributeSelect: (CodedValue) -> Unit, + sliderControlParameters: SliderControlParameters, + onBufferSizeSelect: (Int) -> Unit, + onApplyButtonClicked: (Point) -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + Column( + modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp), + ) { + Text( + text = "Add Feature", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Spacer(Modifier.size(8.dp)) + Text("Attributes:") + + CVDropdown("Status", uiState.status, statusAttributes, onStatusAttributeSelect) + CVDropdown("Protection", uiState.protection, protectionAttributes, onProtectionAttributeSelect) + Spacer(Modifier.size(8.dp)) + + // buffer size displayed and updated in slider + var localBufferSize by remember { mutableIntStateOf(uiState.buffer) } + + var previousBufferSize by remember{ mutableIntStateOf(0)} + if (uiState.buffer != previousBufferSize){ + previousBufferSize = uiState.buffer + localBufferSize = uiState.buffer + } + + // recenter the slider if contingent values change + var bufferRange = sliderControlParameters.minRange..sliderControlParameters.maxRange + if (!bufferRange.contains(localBufferSize)){ + localBufferSize = (bufferRange.start + bufferRange.endInclusive)/2 + } + + Row { + Text("Exclusion area buffer size:") + Spacer(Modifier.weight(1f)) + Text(text = if (localBufferSize > 0) localBufferSize.toString() else "") + } + Slider( + enabled = sliderControlParameters.isEnabled, + value = localBufferSize.toFloat(), + valueRange = sliderControlParameters.minRange.toFloat()..sliderControlParameters.maxRange.toFloat(), + steps = (bufferRange.endInclusive - bufferRange.start).toInt(), + onValueChange = {localBufferSize = it.roundToInt()}, + onValueChangeFinished = { onBufferSizeSelect(localBufferSize) } + ) + HorizontalDivider() + Text("The options will vary depending on which values are selected.") + Button( + modifier = Modifier. + align(Alignment.CenterHorizontally), + onClick = { + // user may have left the slider as is - need to assign a value in that case + onBufferSizeSelect(localBufferSize) + mapPoint?.let { + onApplyButtonClicked(it) + } + coroutineScope.launch{ + bottomSheetState.hide() + }.invokeOnCompletion { + onBottomSheetStateChange(bottomSheetState) + } + }) { Text("Apply") } + + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CVDropdown( + attributeName: String, + codedValue: CodedValue?, + attributeOptions: List, + onNewValueSelect: (CodedValue) -> Unit +){ + var expanded by rememberSaveable { mutableStateOf(false) } + ExposedDropdownMenuBox( + modifier = Modifier.fillMaxWidth(), + onExpandedChange = { if (!attributeOptions.isEmpty()) {expanded = !expanded} }, + expanded = expanded + ) { + + val textValue = codedValue?.name + + OutlinedTextField( + enabled = !attributeOptions.isEmpty(), + modifier = Modifier + .fillMaxWidth() + .menuAnchor(type = MenuAnchorType.PrimaryNotEditable), + value = textValue ?: "", + onValueChange = {}, + label = {Text("Select $attributeName Attribute")} , + readOnly = true + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + attributeOptions.forEach { value -> + DropdownMenuItem( + text = { Text(value.name) }, + onClick = { + expanded = false + onNewValueSelect(value) + } + ) + } + } + } +} From 1210b089295193a4df502f13bf16f6a123fc7182 Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Tue, 3 Dec 2024 12:20:54 +0000 Subject: [PATCH 02/13] Visibility, names, and formatting --- .../components/MapViewModel.kt | 101 ++++++++++++------ .../screens/MainScreen.kt | 62 +++++++---- 2 files changed, 110 insertions(+), 53 deletions(-) diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt index 027434cb3..8d7ac0a66 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt @@ -17,7 +17,6 @@ package com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components import android.app.Application -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -88,10 +87,11 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { // instance of the feature table retrieved from the geodatabase, updates when new feature is added var featureTable: ArcGISFeatureTable? = null + // state flow of UI state private val _uiState = MutableStateFlow(UIState(null, null, 0)) val uiState = _uiState.asStateFlow() - // state flows for possible values + // state flows of possible values for an in-construction feature private val _statusAttributes = MutableStateFlow>(listOf()) private val _protectionAttributes = MutableStateFlow>(listOf()) private val _sliderControlParameters = MutableStateFlow(SliderControlParameters(false, 0, 0)) @@ -103,24 +103,39 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { // create a temporary directory for use with the geodatabase file createGeodatabaseCacheDir() - viewModelScope.launch{ + viewModelScope.launch { // retrieve and load the offline mobile geodatabase file from the cache directory geodatabase.load().getOrElse { - messageDialogVM.showMessageDialog("Error loading GeoDatabase", it.message.toString()) + messageDialogVM.showMessageDialog( + "Error loading GeoDatabase", + it.message.toString() + ) } // get the first geodatabase feature table - val featureTable = geodatabase.featureTables.firstOrNull() ?: - return@launch messageDialogVM.showMessageDialog("Error", "Error retrieving extent of the feature layer") + val featureTable = geodatabase.featureTables.firstOrNull() + ?: return@launch messageDialogVM.showMessageDialog( + "Error", + "No feature table found in geodatabase" + ) // load the geodatabase feature table - featureTable.load().getOrElse { return@launch messageDialogVM.showMessageDialog("Error", it.message.toString()) } + featureTable.load().getOrElse { + return@launch messageDialogVM.showMessageDialog( + "Error loading feature table", + it.message.toString() + ) + } // create and load the feature layer from the feature table val featureLayer = FeatureLayer.createWithFeatureTable(featureTable) // get the full extent of the feature layer val extent = featureLayer.fullExtent + ?: return@launch messageDialogVM.showMessageDialog( + "Error", + "Error retrieving extent of the feature layer" + ) // set the basemap to the offline vector tiled layer, and viewpoint to the feature layer extent arcGISMap = ArcGISMap(Basemap(fillmoreVectorTileLayer)).apply { @@ -145,7 +160,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { * which will need to be cleared before looking up the [geodatabase] again. * A copy of the original geodatabase file is created in the cache folder. */ - fun createGeodatabaseCacheDir(){ + private fun createGeodatabaseCacheDir() { // clear cache directory File(cacheDir.path).deleteRecursively() // copy over the original Geodatabase file to be used in the temp cache directory @@ -158,7 +173,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { * Create buffer graphics for features, and adds the graphics to * the [graphicsOverlay] */ - private suspend fun queryExistingFeatures(){ + private suspend fun queryExistingFeatures() { // clear the existing graphics graphicsOverlay.graphics.clear() @@ -171,26 +186,33 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { val featureQueryResult = featureTable?.queryFeatures(queryParameters)?.getOrThrow() val featureResultList = featureQueryResult?.toList() - if (!featureResultList.isNullOrEmpty()){ + if (!featureResultList.isNullOrEmpty()) { // create list of graphics for each query result val graphics = featureResultList.map { createGraphic(it) } // add the graphics to the graphics overlay graphicsOverlay.graphics.addAll(graphics) + } else { + messageDialogVM.showMessageDialog( + "Error", + "No features found with BufferSize > 0" + ) } } /** * Creates and returns a graphic using the attributes of the given [feature] */ - fun createGraphic(feature: Feature): Graphic{ + private fun createGraphic(feature: Feature): Graphic { // create the outline for the buffer symbol val lineSymbol = SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.black, 2f) // create the buffer symbol - val bufferSymbol = SimpleFillSymbol(SimpleFillSymbolStyle.ForwardDiagonal, Color.red, lineSymbol) + val bufferSymbol = + SimpleFillSymbol(SimpleFillSymbolStyle.ForwardDiagonal, Color.red, lineSymbol) // get the feature's buffer size val bufferSize = feature.attributes["BufferSize"] as Int // get a polygon using the feature's buffer size and geometry - val polygon = feature.geometry?.let { GeometryEngine.bufferOrNull(it, bufferSize.toDouble()) } + val polygon = + feature.geometry?.let { GeometryEngine.bufferOrNull(it, bufferSize.toDouble()) } // create a graphic using the geometry and fill symbol return Graphic(polygon, bufferSymbol) @@ -200,7 +222,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { * Retrieve the status fields, add the fields to a ContingentValueDomain. * Used to display options in the UI. */ - fun statusAttributes(): List{ + private fun statusAttributes(): List { val statusField = featureTable?.fields?.find { field -> field.name == "Status" } val codedValueDomain = statusField?.domain as CodedValueDomain return codedValueDomain.codedValues @@ -208,10 +230,13 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { fun onStatusAttributeSelect(codedValue: CodedValue) { val contingentValuesDefinition = featureTable?.contingentValuesDefinition - if (contingentValuesDefinition != null){ - viewModelScope.launch{ + if (contingentValuesDefinition != null) { + viewModelScope.launch { contingentValuesDefinition.load().getOrElse { - messageDialogVM.showMessageDialog("Error loading the contingent values definition", it.message.toString()) + messageDialogVM.showMessageDialog( + "Error", + "Error loading the contingent values definition" + ) } feature = featureTable?.createFeature() as ArcGISFeature @@ -222,11 +247,14 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { _protectionAttributes.value = protectionAttributes() } } else { - messageDialogVM.showMessageDialog("Error", "Error retrieving ContingentValuesDefinition from the feature table") + messageDialogVM.showMessageDialog( + "Error", + "Error retrieving ContingentValuesDefinition from the feature table" + ) } } - fun protectionAttributes(): List { + private fun protectionAttributes(): List { // get the contingent value results with the feature for the protection field val contingentValuesResult = feature?.let { featureTable?.getContingentValuesOrNull(it, "Protection") @@ -236,28 +264,35 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { val contingentValues = contingentValuesResult?.byFieldGroup?.get("ProtectionFieldGroup") // convert the list of ContingentValues to a list of CodedValue - val protectionCodedValues : List = contingentValues?.map { (it as ContingentCodedValue).codedValue } ?: - listOf().also { messageDialogVM.showMessageDialog("Error", "Error getting coded values by field group") } + val protectionCodedValues: List = + contingentValues?.map { (it as ContingentCodedValue).codedValue } + ?: listOf().also { + messageDialogVM.showMessageDialog( + "Error", + "Error getting coded values by field group" + ) + } return protectionCodedValues } - fun onProtectionAttributeSelect(codedValue: CodedValue){ + fun onProtectionAttributeSelect(codedValue: CodedValue) { feature?.attributes?.set("Protection", codedValue.code) _uiState.value = _uiState.value.copy(protection = codedValue) _sliderControlParameters.value = bufferAttributes() } - fun bufferAttributes(): SliderControlParameters{ + private fun bufferAttributes(): SliderControlParameters { val contingentValueResult = feature?.let { featureTable?.getContingentValuesOrNull(it, "BufferSize") } - val bufferSizeRangeValue = contingentValueResult?.byFieldGroup?.get("BufferSizeFieldGroup")?.get(0) as ContingentRangeValue + val bufferSizeRangeValue = contingentValueResult?.byFieldGroup?.get("BufferSizeFieldGroup") + ?.get(0) as ContingentRangeValue val minValue = bufferSizeRangeValue.minValue as Int val maxValue = bufferSizeRangeValue.maxValue as Int - val sliderControlParameters = if (maxValue > 0){ + val sliderControlParameters = if (maxValue > 0) { SliderControlParameters(true, minValue, maxValue) } else { SliderControlParameters(false, 0, 0) @@ -266,9 +301,9 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { return sliderControlParameters } - fun onBufferSizeSelect(bufferSize: Int){ + fun onBufferSizeSelect(bufferSize: Int) { feature?.attributes?.set("BufferSize", bufferSize) - _uiState.value = _uiState.value.copy(buffer=bufferSize) + _uiState.value = _uiState.value.copy(buffer = bufferSize) } /** @@ -311,10 +346,12 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } } } else { - messageDialogVM.showMessageDialog("Error", "Invalid contingent values: " + (contingencyViolations.size) + " violations found.") - contingencyViolations.forEach { - Log.e("ContingencyViolation", it.fieldGroup.name) - } + val violations = contingencyViolations.map { violation -> violation.fieldGroup.name } + .joinToString(separator = "\n") + messageDialogVM.showMessageDialog( + "Invalid contingent values", + "${contingencyViolations.size} violations found:\n" + violations + ) } } @@ -325,7 +362,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { */ fun clearFeature() { feature = null - _uiState.value = UIState(null,null,0) + _uiState.value = UIState(null, null, 0) _sliderControlParameters.value = SliderControlParameters(false, 0, 0) } diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt index a3e33efec..92da2fe89 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetState import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -72,11 +73,13 @@ fun MainScreen(sampleName: String) { // create a ViewModel to handle MapView interactions val mapViewModel: MapViewModel = viewModel() + // flows from view model for displaying state in UI val uiState by mapViewModel.uiState.collectAsStateWithLifecycle() val statusAttributes by mapViewModel.statusAttributes.collectAsStateWithLifecycle() val protectionAttributes by mapViewModel.protectionAttributes.collectAsStateWithLifecycle() val sliderControlParameters by mapViewModel.sliderControlParameters.collectAsStateWithLifecycle() + // point on map tapped by user var mapPoint by remember { mutableStateOf(null) } Scaffold( @@ -93,7 +96,7 @@ fun MainScreen(sampleName: String) { arcGISMap = mapViewModel.arcGISMap, graphicsOverlays = listOf(mapViewModel.graphicsOverlay), onSingleTapConfirmed = { - it.mapPoint.let {point -> + it.mapPoint.let { point -> mapPoint = point showBottomSheet = true } @@ -115,7 +118,7 @@ fun MainScreen(sampleName: String) { mapViewModel.clearFeature() } } - CVBSContents( + BottomSheetContents( mapPoint, bottomSheetState, onBottomSheetStateChanged, @@ -132,6 +135,7 @@ fun MainScreen(sampleName: String) { } } + // message dialog view model for displaying error messages mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( @@ -147,7 +151,7 @@ fun MainScreen(sampleName: String) { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CVBSContents( +fun BottomSheetContents( mapPoint: Point?, bottomSheetState: SheetState, onBottomSheetStateChange: (SheetState) -> Unit, @@ -173,23 +177,25 @@ fun CVBSContents( Spacer(Modifier.size(8.dp)) Text("Attributes:") - CVDropdown("Status", uiState.status, statusAttributes, onStatusAttributeSelect) - CVDropdown("Protection", uiState.protection, protectionAttributes, onProtectionAttributeSelect) + // dropdown boxes for selecting feature status/protection + AttributeDropdown("Status", uiState.status, statusAttributes, onStatusAttributeSelect) + AttributeDropdown("Protection", uiState.protection, protectionAttributes, onProtectionAttributeSelect) Spacer(Modifier.size(8.dp)) // buffer size displayed and updated in slider var localBufferSize by remember { mutableIntStateOf(uiState.buffer) } - var previousBufferSize by remember{ mutableIntStateOf(0)} - if (uiState.buffer != previousBufferSize){ + // update buffer size if it changes in the view model + var previousBufferSize by remember { mutableIntStateOf(0) } + if (uiState.buffer != previousBufferSize) { previousBufferSize = uiState.buffer localBufferSize = uiState.buffer } // recenter the slider if contingent values change var bufferRange = sliderControlParameters.minRange..sliderControlParameters.maxRange - if (!bufferRange.contains(localBufferSize)){ - localBufferSize = (bufferRange.start + bufferRange.endInclusive)/2 + if (!bufferRange.contains(localBufferSize)) { + localBufferSize = (bufferRange.start + bufferRange.endInclusive) / 2 } Row { @@ -202,42 +208,56 @@ fun CVBSContents( value = localBufferSize.toFloat(), valueRange = sliderControlParameters.minRange.toFloat()..sliderControlParameters.maxRange.toFloat(), steps = (bufferRange.endInclusive - bufferRange.start).toInt(), - onValueChange = {localBufferSize = it.roundToInt()}, - onValueChangeFinished = { onBufferSizeSelect(localBufferSize) } + onValueChange = { localBufferSize = it.roundToInt() }, + onValueChangeFinished = { onBufferSizeSelect(localBufferSize) }, + track = { sliderState -> + SliderDefaults.Track( + enabled = sliderControlParameters.isEnabled, + sliderState = sliderState, + drawStopIndicator = null, + drawTick = { _, _ -> } + ) + } ) HorizontalDivider() Text("The options will vary depending on which values are selected.") Button( - modifier = Modifier. - align(Alignment.CenterHorizontally), + modifier = Modifier.align(Alignment.CenterHorizontally), onClick = { - // user may have left the slider as is - need to assign a value in that case + // user may not have interacted with the slider - need to assign a value onBufferSizeSelect(localBufferSize) mapPoint?.let { onApplyButtonClicked(it) } - coroutineScope.launch{ + coroutineScope.launch { bottomSheetState.hide() }.invokeOnCompletion { onBottomSheetStateChange(bottomSheetState) } - }) { Text("Apply") } + } + ) { + Text("Apply") + } } } @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CVDropdown( +fun AttributeDropdown( attributeName: String, codedValue: CodedValue?, attributeOptions: List, onNewValueSelect: (CodedValue) -> Unit -){ +) { var expanded by rememberSaveable { mutableStateOf(false) } ExposedDropdownMenuBox( modifier = Modifier.fillMaxWidth(), - onExpandedChange = { if (!attributeOptions.isEmpty()) {expanded = !expanded} }, + onExpandedChange = { + if (!attributeOptions.isEmpty()) { + expanded = !expanded + } + }, expanded = expanded ) { @@ -250,9 +270,9 @@ fun CVDropdown( .menuAnchor(type = MenuAnchorType.PrimaryNotEditable), value = textValue ?: "", onValueChange = {}, - label = {Text("Select $attributeName Attribute")} , + label = { Text("Select $attributeName Attribute") }, readOnly = true - ) + ) ExposedDropdownMenu( expanded = expanded, From b99f10b9e8b5152c8d221bcaa31c5394cd22792d Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Wed, 11 Dec 2024 10:03:31 +0000 Subject: [PATCH 03/13] Changes after PR review --- .../README.md | 2 +- .../README.metadata.json | 2 +- .../components/MapViewModel.kt | 61 +++--- .../screens/MainScreen.kt | 70 +++---- .../main/res/layout/add_feature_layout.xml | 179 ------------------ ...s_with_contingent_values_activity_main.xml | 10 - .../src/main/res/layout/list_item.xml | 8 - .../src/main/res/values/strings.xml | 11 +- 8 files changed, 68 insertions(+), 275 deletions(-) delete mode 100644 samples/add-features-with-contingent-values/src/main/res/layout/add_feature_layout.xml delete mode 100644 samples/add-features-with-contingent-values/src/main/res/layout/add_features_with_contingent_values_activity_main.xml delete mode 100644 samples/add-features-with-contingent-values/src/main/res/layout/list_item.xml diff --git a/samples/add-features-with-contingent-values/README.md b/samples/add-features-with-contingent-values/README.md index 52c2d4885..1f32214cf 100644 --- a/samples/add-features-with-contingent-values/README.md +++ b/samples/add-features-with-contingent-values/README.md @@ -48,7 +48,7 @@ The mobile geodatabase contains birds nests in the Fillmore area, defined with c ## Additional information -This sample uses the GeoCompose Toolkit module to be able to implement a Composable MapView. +This sample uses the `geoview-compose` module of the ArcGIS Maps SDK for Kotlin Toolkit to implement a Composable MapView. Learn more about contingent values and how to utilize them on the [ArcGIS Pro documentation](https://pro.arcgis.com/en/pro-app/latest/help/data/geodatabases/overview/contingent-values.htm). ## Tags diff --git a/samples/add-features-with-contingent-values/README.metadata.json b/samples/add-features-with-contingent-values/README.metadata.json index c7c889cd4..d50f8ed51 100644 --- a/samples/add-features-with-contingent-values/README.metadata.json +++ b/samples/add-features-with-contingent-values/README.metadata.json @@ -38,8 +38,8 @@ ], "snippets": [ "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt", - "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/DownloadActivity.kt", "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/MainActivity.kt", + "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/DownloadActivity.kt", "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt" ], "title": "Add features with contingent values" diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt index 8d7ac0a66..403de4626 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt @@ -56,7 +56,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow class MapViewModel(application: Application) : AndroidViewModel(application) { - val cacheDir = application.cacheDir + private val cacheDir: File = application.cacheDir private val provisionPath: String by lazy { application.getExternalFilesDir(null)?.path.toString() + @@ -71,7 +71,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { val messageDialogVM = MessageDialogViewModel() // offline vector tiled layer to be used as a basemap - val fillmoreVectorTileLayer = ArcGISVectorTiledLayer("$provisionPath/FillmoreTopographicMap.vtpk") + private val fillmoreVectorTileLayer = ArcGISVectorTiledLayer("$provisionPath/FillmoreTopographicMap.vtpk") // mobile database containing offline feature data val geodatabase: Geodatabase by lazy { @@ -85,19 +85,11 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { var feature: ArcGISFeature? = null // instance of the feature table retrieved from the geodatabase, updates when new feature is added - var featureTable: ArcGISFeatureTable? = null + private var featureTable: ArcGISFeatureTable? = null // state flow of UI state - private val _uiState = MutableStateFlow(UIState(null, null, 0)) - val uiState = _uiState.asStateFlow() - - // state flows of possible values for an in-construction feature - private val _statusAttributes = MutableStateFlow>(listOf()) - private val _protectionAttributes = MutableStateFlow>(listOf()) - private val _sliderControlParameters = MutableStateFlow(SliderControlParameters(false, 0, 0)) - val statusAttributes = _statusAttributes.asStateFlow() - val protectionAttributes = _protectionAttributes.asStateFlow() - val sliderControlParameters = _sliderControlParameters.asStateFlow() + private val _featureEditState = MutableStateFlow(FeatureEditState()) + val featureEditState = _featureEditState.asStateFlow() init { // create a temporary directory for use with the geodatabase file @@ -150,7 +142,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { queryExistingFeatures() // get status attributes for new features - _statusAttributes.value = statusAttributes() + _featureEditState.value = _featureEditState.value.copy(statusAttributes = statusAttributes()) } } @@ -170,7 +162,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } /** - * Create buffer graphics for features, and adds the graphics to + * Creates buffer graphics for features, and adds the graphics to * the [graphicsOverlay] */ private suspend fun queryExistingFeatures() { @@ -183,7 +175,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } // query the features using the queryParameters on the featureTable - val featureQueryResult = featureTable?.queryFeatures(queryParameters)?.getOrThrow() + val featureQueryResult = featureTable?.queryFeatures(queryParameters)?.getOrNull() val featureResultList = featureQueryResult?.toList() if (!featureResultList.isNullOrEmpty()) { @@ -242,9 +234,11 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { feature = featureTable?.createFeature() as ArcGISFeature feature?.attributes?.set("Status", codedValue.code) - _uiState.value = UIState(codedValue, null, 0) - - _protectionAttributes.value = protectionAttributes() + _featureEditState.value = FeatureEditState( + status=codedValue, + statusAttributes = statusAttributes(), + protectionAttributes = protectionAttributes() + ) } } else { messageDialogVM.showMessageDialog( @@ -278,8 +272,10 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { fun onProtectionAttributeSelect(codedValue: CodedValue) { feature?.attributes?.set("Protection", codedValue.code) - _uiState.value = _uiState.value.copy(protection = codedValue) - _sliderControlParameters.value = bufferAttributes() + _featureEditState.value = _featureEditState.value.copy( + protection = codedValue, + sliderControlParameters = bufferAttributes() + ) } private fun bufferAttributes(): SliderControlParameters { @@ -295,7 +291,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { val sliderControlParameters = if (maxValue > 0) { SliderControlParameters(true, minValue, maxValue) } else { - SliderControlParameters(false, 0, 0) + SliderControlParameters() } return sliderControlParameters @@ -303,7 +299,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { fun onBufferSizeSelect(bufferSize: Int) { feature?.attributes?.set("BufferSize", bufferSize) - _uiState.value = _uiState.value.copy(buffer = bufferSize) + _featureEditState.value = _featureEditState.value.copy(buffer = bufferSize) } /** @@ -346,8 +342,9 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } } } else { - val violations = contingencyViolations.map { violation -> violation.fieldGroup.name } - .joinToString(separator = "\n") + val violations = contingencyViolations.joinToString(separator = "\n") { + violation -> violation.fieldGroup.name + } messageDialogVM.showMessageDialog( "Invalid contingent values", "${contingencyViolations.size} violations found:\n" + violations @@ -362,8 +359,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { */ fun clearFeature() { feature = null - _uiState.value = UIState(null, null, 0) - _sliderControlParameters.value = SliderControlParameters(false, 0, 0) + _featureEditState.value = FeatureEditState(statusAttributes = statusAttributes()) } } @@ -371,10 +367,17 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { /** * Enable status, maximum, and minimum values for the buffer size slider */ -data class SliderControlParameters(val isEnabled: Boolean, val minRange: Int, val maxRange: Int) +data class SliderControlParameters(val isEnabled: Boolean = false, val minRange: Int = 0, val maxRange: Int = 0) /** * Currently selected status, protection, and buffer attributes for the feature under construction, * used to update the UI. */ -data class UIState(val status: CodedValue?, val protection: CodedValue?, val buffer: Int) +data class FeatureEditState( + val status: CodedValue? = null, + val protection: CodedValue? = null, + val buffer: Int = 0, + val statusAttributes: List = listOf(), + val protectionAttributes: List = listOf(), + val sliderControlParameters: SliderControlParameters = SliderControlParameters() +) diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt index 92da2fe89..b12c643c5 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt @@ -51,16 +51,17 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arcgismaps.data.CodedValue import com.arcgismaps.geometry.Point import com.arcgismaps.toolkit.geoviewcompose.MapView +import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.R import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.MapViewModel import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar -import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.SliderControlParameters -import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.UIState +import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.FeatureEditState import kotlin.math.roundToInt import kotlinx.coroutines.launch @@ -74,10 +75,7 @@ fun MainScreen(sampleName: String) { val mapViewModel: MapViewModel = viewModel() // flows from view model for displaying state in UI - val uiState by mapViewModel.uiState.collectAsStateWithLifecycle() - val statusAttributes by mapViewModel.statusAttributes.collectAsStateWithLifecycle() - val protectionAttributes by mapViewModel.protectionAttributes.collectAsStateWithLifecycle() - val sliderControlParameters by mapViewModel.sliderControlParameters.collectAsStateWithLifecycle() + val featureEditState by mapViewModel.featureEditState.collectAsStateWithLifecycle() // point on map tapped by user var mapPoint by remember { mutableStateOf(null) } @@ -113,7 +111,7 @@ fun MainScreen(sampleName: String) { } ) { val onBottomSheetStateChanged = { state: SheetState -> - if (!bottomSheetState.isVisible) { + if (!state.isVisible) { showBottomSheet = false mapViewModel.clearFeature() } @@ -122,12 +120,9 @@ fun MainScreen(sampleName: String) { mapPoint, bottomSheetState, onBottomSheetStateChanged, - uiState, - statusAttributes, + featureEditState, mapViewModel::onStatusAttributeSelect, - protectionAttributes, mapViewModel::onProtectionAttributeSelect, - sliderControlParameters, mapViewModel::onBufferSizeSelect, mapViewModel::validateContingency ) @@ -155,12 +150,9 @@ fun BottomSheetContents( mapPoint: Point?, bottomSheetState: SheetState, onBottomSheetStateChange: (SheetState) -> Unit, - uiState: UIState, - statusAttributes: List, + featureEditState: FeatureEditState, onStatusAttributeSelect: (CodedValue) -> Unit, - protectionAttributes: List, onProtectionAttributeSelect: (CodedValue) -> Unit, - sliderControlParameters: SliderControlParameters, onBufferSizeSelect: (Int) -> Unit, onApplyButtonClicked: (Point) -> Unit, ) { @@ -169,50 +161,46 @@ fun BottomSheetContents( modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp), ) { Text( - text = "Add Feature", + text = stringResource(R.string.add_feature), style = MaterialTheme.typography.titleMedium, modifier = Modifier.align(Alignment.CenterHorizontally) ) Spacer(Modifier.size(8.dp)) - Text("Attributes:") + Text(stringResource(R.string.attributes)) // dropdown boxes for selecting feature status/protection - AttributeDropdown("Status", uiState.status, statusAttributes, onStatusAttributeSelect) - AttributeDropdown("Protection", uiState.protection, protectionAttributes, onProtectionAttributeSelect) + AttributeDropdown(stringResource(R.string.status), featureEditState.status, featureEditState.statusAttributes, onStatusAttributeSelect) + AttributeDropdown(stringResource(R.string.protection), featureEditState.protection, featureEditState.protectionAttributes, onProtectionAttributeSelect) Spacer(Modifier.size(8.dp)) // buffer size displayed and updated in slider - var localBufferSize by remember { mutableIntStateOf(uiState.buffer) } - - // update buffer size if it changes in the view model - var previousBufferSize by remember { mutableIntStateOf(0) } - if (uiState.buffer != previousBufferSize) { - previousBufferSize = uiState.buffer - localBufferSize = uiState.buffer - } + var bufferSize by remember(key1= featureEditState) { mutableIntStateOf(featureEditState.buffer) } // recenter the slider if contingent values change - var bufferRange = sliderControlParameters.minRange..sliderControlParameters.maxRange - if (!bufferRange.contains(localBufferSize)) { - localBufferSize = (bufferRange.start + bufferRange.endInclusive) / 2 + val minRange = featureEditState.sliderControlParameters.minRange + val maxRange = featureEditState.sliderControlParameters.maxRange + val bufferRange = minRange..maxRange + if (!bufferRange.contains(bufferSize)) { + bufferSize = (bufferRange.start + bufferRange.endInclusive) / 2 } Row { - Text("Exclusion area buffer size:") + Text(stringResource(R.string.exclusion_area_buffer_size)) Spacer(Modifier.weight(1f)) - Text(text = if (localBufferSize > 0) localBufferSize.toString() else "") + Text(text = if (bufferSize > 0) bufferSize.toString() else "") } + Slider( - enabled = sliderControlParameters.isEnabled, - value = localBufferSize.toFloat(), - valueRange = sliderControlParameters.minRange.toFloat()..sliderControlParameters.maxRange.toFloat(), + enabled = featureEditState.sliderControlParameters.isEnabled, + value = bufferSize.toFloat(), + valueRange = minRange.toFloat()..maxRange.toFloat(), steps = (bufferRange.endInclusive - bufferRange.start).toInt(), - onValueChange = { localBufferSize = it.roundToInt() }, - onValueChangeFinished = { onBufferSizeSelect(localBufferSize) }, + onValueChange = { bufferSize = it.roundToInt() }, + onValueChangeFinished = { onBufferSizeSelect(bufferSize) }, track = { sliderState -> SliderDefaults.Track( - enabled = sliderControlParameters.isEnabled, + enabled = featureEditState.sliderControlParameters.isEnabled, sliderState = sliderState, drawStopIndicator = null, drawTick = { _, _ -> } @@ -220,12 +208,12 @@ fun BottomSheetContents( } ) HorizontalDivider() - Text("The options will vary depending on which values are selected.") + Text(stringResource(R.string.contingent_note)) Button( modifier = Modifier.align(Alignment.CenterHorizontally), onClick = { // user may not have interacted with the slider - need to assign a value - onBufferSizeSelect(localBufferSize) + onBufferSizeSelect(bufferSize) mapPoint?.let { onApplyButtonClicked(it) } @@ -236,7 +224,7 @@ fun BottomSheetContents( } } ) { - Text("Apply") + Text(stringResource(R.string.apply)) } } diff --git a/samples/add-features-with-contingent-values/src/main/res/layout/add_feature_layout.xml b/samples/add-features-with-contingent-values/src/main/res/layout/add_feature_layout.xml deleted file mode 100644 index d36122720..000000000 --- a/samples/add-features-with-contingent-values/src/main/res/layout/add_feature_layout.xml +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/add-features-with-contingent-values/src/main/res/layout/add_features_with_contingent_values_activity_main.xml b/samples/add-features-with-contingent-values/src/main/res/layout/add_features_with_contingent_values_activity_main.xml deleted file mode 100644 index b5c4aee8f..000000000 --- a/samples/add-features-with-contingent-values/src/main/res/layout/add_features_with_contingent_values_activity_main.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/samples/add-features-with-contingent-values/src/main/res/layout/list_item.xml b/samples/add-features-with-contingent-values/src/main/res/layout/list_item.xml deleted file mode 100644 index a52f61631..000000000 --- a/samples/add-features-with-contingent-values/src/main/res/layout/list_item.xml +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/samples/add-features-with-contingent-values/src/main/res/values/strings.xml b/samples/add-features-with-contingent-values/src/main/res/values/strings.xml index f897f0aa1..7c7a0f282 100644 --- a/samples/add-features-with-contingent-values/src/main/res/values/strings.xml +++ b/samples/add-features-with-contingent-values/src/main/res/values/strings.xml @@ -1,11 +1,10 @@ Add features with contingent values - The options will vary depending on which values are selected + The options will vary depending on which values are selected. Add Feature Apply - Cancel - Set the attributes: - Select status attribute - Select protection attribute - Exclusion area buffer size + Attributes: + Status + Protection + Exclusion area buffer size: From 6e20e477cdb190d4abcaecd9fa3d272317eaedf8 Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Wed, 11 Dec 2024 10:07:07 +0000 Subject: [PATCH 04/13] Metadata --- .../add-features-with-contingent-values/README.metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/add-features-with-contingent-values/README.metadata.json b/samples/add-features-with-contingent-values/README.metadata.json index d50f8ed51..c7c889cd4 100644 --- a/samples/add-features-with-contingent-values/README.metadata.json +++ b/samples/add-features-with-contingent-values/README.metadata.json @@ -38,8 +38,8 @@ ], "snippets": [ "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt", - "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/MainActivity.kt", "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/DownloadActivity.kt", + "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/MainActivity.kt", "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt" ], "title": "Add features with contingent values" From cab5643749a9f8f9e4315e8300549551b5054476 Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Fri, 13 Dec 2024 18:15:21 +0000 Subject: [PATCH 05/13] Changes after PR review --- .../README.md | 4 +- .../README.metadata.json | 4 +- .../components/MapViewModel.kt | 151 +++++++----------- .../screens/MainScreen.kt | 51 +++--- .../src/main/res/values/strings.xml | 2 + 5 files changed, 100 insertions(+), 112 deletions(-) diff --git a/samples/add-features-with-contingent-values/README.md b/samples/add-features-with-contingent-values/README.md index 1f32214cf..269fdc4be 100644 --- a/samples/add-features-with-contingent-values/README.md +++ b/samples/add-features-with-contingent-values/README.md @@ -12,7 +12,7 @@ For example, a field crew working in a sensitive habitat area may be required to ## How to use the sample -Tap on the map to add a feature symbolizing a bird's nest. Then choose values describing the nest's status, protection, and buffer size. Notice how different values are available depending on the values of preceding fields. Once the contingent values are validated, tap "Done" to add the feature to the map. +Tap on the map to add a feature symbolizing a bird's nest. Then choose values describing the nest's status, protection, and buffer size. Notice how different values are available depending on the values of preceding fields. Once the contingent values are validated, tap "Apply" to add the feature to the map. ## How it works @@ -53,4 +53,4 @@ Learn more about contingent values and how to utilize them on the [ArcGIS Pro do ## Tags -coded values, contingent values, feature table, geodatabase, geoviewcompose, toolkit +coded values, compose, contingent values, feature table, geodatabase, geoview, mapview, toolkit diff --git a/samples/add-features-with-contingent-values/README.metadata.json b/samples/add-features-with-contingent-values/README.metadata.json index c7c889cd4..6e7b23a8d 100644 --- a/samples/add-features-with-contingent-values/README.metadata.json +++ b/samples/add-features-with-contingent-values/README.metadata.json @@ -8,10 +8,12 @@ ], "keywords": [ "coded values", + "compose", "contingent values", "feature table", "geodatabase", - "geoviewcompose", + "geoview", + "mapview", "toolkit", "ArcGISFeatureTable", "CodedValue", diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt index 403de4626..586ada100 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt @@ -44,8 +44,6 @@ import com.arcgismaps.mapping.symbology.SimpleFillSymbol import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle import com.arcgismaps.mapping.symbology.SimpleLineSymbol import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle -import com.arcgismaps.mapping.symbology.SimpleMarkerSymbol -import com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyle import com.arcgismaps.mapping.view.Graphic import com.arcgismaps.mapping.view.GraphicsOverlay import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.R @@ -56,41 +54,36 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow class MapViewModel(application: Application) : AndroidViewModel(application) { - private val cacheDir: File = application.cacheDir - - private val provisionPath: String by lazy { - application.getExternalFilesDir(null)?.path.toString() + - File.separator + - application.getString(R.string.add_features_with_contingent_values_app_name) - } - // Create an empty map, to be updated once data is loaded from the feature table var arcGISMap by mutableStateOf(ArcGISMap()) // Create a message dialog view model for handling error messages val messageDialogVM = MessageDialogViewModel() + // graphics overlay used to add feature graphics to the map + val graphicsOverlay = GraphicsOverlay() + + // state flow of UI state + private val _featureEditState = MutableStateFlow(FeatureEditState()) + val featureEditState = _featureEditState.asStateFlow() + + private val cacheDir: File = application.cacheDir + + private val provisionPath: String = application.getExternalFilesDir(null)?.path.toString() + + File.separator + application.getString(R.string.add_features_with_contingent_values_app_name) + // offline vector tiled layer to be used as a basemap private val fillmoreVectorTileLayer = ArcGISVectorTiledLayer("$provisionPath/FillmoreTopographicMap.vtpk") // mobile database containing offline feature data - val geodatabase: Geodatabase by lazy { - Geodatabase("${cacheDir.path}/ContingentValuesBirdNests.geodatabase") - } - - // graphics overlay used to add feature graphics to the map - val graphicsOverlay = GraphicsOverlay() + private val geodatabase = Geodatabase("${cacheDir.path}/ContingentValuesBirdNests.geodatabase") // instance of the contingent feature to be added to the map - var feature: ArcGISFeature? = null + private var feature: ArcGISFeature? = null // instance of the feature table retrieved from the geodatabase, updates when new feature is added private var featureTable: ArcGISFeatureTable? = null - // state flow of UI state - private val _featureEditState = MutableStateFlow(FeatureEditState()) - val featureEditState = _featureEditState.asStateFlow() - init { // create a temporary directory for use with the geodatabase file createGeodatabaseCacheDir() @@ -119,6 +112,17 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { ) } + // get the contingent values definition from the feature table and load it + val contingentValuesDefinition = featureTable.contingentValuesDefinition + viewModelScope.launch { + contingentValuesDefinition.load().getOrElse { + messageDialogVM.showMessageDialog( + "Error", + "Error loading the contingent values definition" + ) + } + } + // create and load the feature layer from the feature table val featureLayer = FeatureLayer.createWithFeatureTable(featureTable) @@ -139,10 +143,10 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { this@MapViewModel.featureTable = featureTable // add buffer graphics for the feature layer - queryExistingFeatures() + showBufferGraphics() // get status attributes for new features - _featureEditState.value = _featureEditState.value.copy(statusAttributes = statusAttributes()) + _featureEditState.value = _featureEditState.value.copy(statusAttributes = featureTable.statusFieldCodedValues()) } } @@ -162,10 +166,10 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } /** - * Creates buffer graphics for features, and adds the graphics to + * Creates buffer graphics for features in the [featureTable], and adds the graphics to * the [graphicsOverlay] */ - private suspend fun queryExistingFeatures() { + private suspend fun showBufferGraphics() { // clear the existing graphics graphicsOverlay.graphics.clear() @@ -175,12 +179,12 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } // query the features using the queryParameters on the featureTable - val featureQueryResult = featureTable?.queryFeatures(queryParameters)?.getOrNull() + val featureQueryResult = featureTable!!.queryFeatures(queryParameters).getOrNull() val featureResultList = featureQueryResult?.toList() if (!featureResultList.isNullOrEmpty()) { // create list of graphics for each query result - val graphics = featureResultList.map { createGraphic(it) } + val graphics = featureResultList.map { createBufferGraphic(it) } // add the graphics to the graphics overlay graphicsOverlay.graphics.addAll(graphics) } else { @@ -194,7 +198,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { /** * Creates and returns a graphic using the attributes of the given [feature] */ - private fun createGraphic(feature: Feature): Graphic { + private fun createBufferGraphic(feature: Feature): Graphic { // create the outline for the buffer symbol val lineSymbol = SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.black, 2f) // create the buffer symbol @@ -214,44 +218,30 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { * Retrieve the status fields, add the fields to a ContingentValueDomain. * Used to display options in the UI. */ - private fun statusAttributes(): List { - val statusField = featureTable?.fields?.find { field -> field.name == "Status" } + private fun ArcGISFeatureTable.statusFieldCodedValues(): List { + val statusField = fields.find { field -> field.name == "Status" } val codedValueDomain = statusField?.domain as CodedValueDomain return codedValueDomain.codedValues } - fun onStatusAttributeSelect(codedValue: CodedValue) { - val contingentValuesDefinition = featureTable?.contingentValuesDefinition - if (contingentValuesDefinition != null) { - viewModelScope.launch { - contingentValuesDefinition.load().getOrElse { - messageDialogVM.showMessageDialog( - "Error", - "Error loading the contingent values definition" - ) - } + fun onStatusAttributeSelected(codedValue: CodedValue) { + viewModelScope.launch { - feature = featureTable?.createFeature() as ArcGISFeature - feature?.attributes?.set("Status", codedValue.code) + feature = featureTable!!.createFeature() as ArcGISFeature + feature?.attributes?.set("Status", codedValue.code) - _featureEditState.value = FeatureEditState( - status=codedValue, - statusAttributes = statusAttributes(), - protectionAttributes = protectionAttributes() - ) - } - } else { - messageDialogVM.showMessageDialog( - "Error", - "Error retrieving ContingentValuesDefinition from the feature table" + _featureEditState.value = FeatureEditState( + status=codedValue, + statusAttributes = featureTable!!.statusFieldCodedValues(), + protectionAttributes = featureTable!!.protectionFieldCodedValues() ) } } - private fun protectionAttributes(): List { + private fun ArcGISFeatureTable.protectionFieldCodedValues(): List { // get the contingent value results with the feature for the protection field val contingentValuesResult = feature?.let { - featureTable?.getContingentValuesOrNull(it, "Protection") + getContingentValuesOrNull(it, "Protection") } // get the list of contingent values by field group @@ -270,34 +260,23 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { return protectionCodedValues } - fun onProtectionAttributeSelect(codedValue: CodedValue) { + fun onProtectionAttributeSelected(codedValue: CodedValue) { feature?.attributes?.set("Protection", codedValue.code) _featureEditState.value = _featureEditState.value.copy( protection = codedValue, - sliderControlParameters = bufferAttributes() + bufferRange = featureTable!!.bufferRange(), ) } - private fun bufferAttributes(): SliderControlParameters { + private fun ArcGISFeatureTable.bufferRange(): ContingentRangeValue? { val contingentValueResult = feature?.let { - featureTable?.getContingentValuesOrNull(it, "BufferSize") - } - - val bufferSizeRangeValue = contingentValueResult?.byFieldGroup?.get("BufferSizeFieldGroup") - ?.get(0) as ContingentRangeValue - val minValue = bufferSizeRangeValue.minValue as Int - val maxValue = bufferSizeRangeValue.maxValue as Int - - val sliderControlParameters = if (maxValue > 0) { - SliderControlParameters(true, minValue, maxValue) - } else { - SliderControlParameters() + getContingentValuesOrNull(it, "BufferSize") } - return sliderControlParameters + return contingentValueResult?.byFieldGroup?.get("BufferSizeFieldGroup")?.get(0) as? ContingentRangeValue } - fun onBufferSizeSelect(bufferSize: Int) { + fun onBufferSizeSelected(bufferSize: Int) { feature?.attributes?.set("BufferSize", bufferSize) _featureEditState.value = _featureEditState.value.copy(buffer = bufferSize) } @@ -307,36 +286,33 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { * If contingencies are valid, then display [feature] on the [mapPoint] */ fun validateContingency(mapPoint: Point) { + val resources = getApplication().resources // check if all the features have been set if (featureTable == null) { - messageDialogVM.showMessageDialog("Input all values to add a feature to the map") + messageDialogVM.showMessageDialog(resources.getString(R.string.input_all_values)) return } // validate the feature's contingencies val contingencyViolations = feature?.let { - featureTable?.validateContingencyConstraints(it) - } ?: return messageDialogVM.showMessageDialog("No feature attribute was selected") + featureTable!!.validateContingencyConstraints(it) + } ?: return messageDialogVM.showMessageDialog(resources.getString(R.string.no_feature_created)) - // if there are no contingency violations + // if there are no contingency violations the feature is valid and ready to add to the feature table if (contingencyViolations.isEmpty()) { - // the feature is valid and ready to add to the feature table - // create a symbol to represent a bird's nest - val symbol = SimpleMarkerSymbol(SimpleMarkerSymbolStyle.Circle, Color.black, 11F) - // add the graphic to the graphics overlay - graphicsOverlay.graphics.add(Graphic(mapPoint, symbol)) // set the geometry of the feature to the map point feature?.geometry = mapPoint - // create the graphic of the feature - val graphic = feature?.let { createGraphic(it) } + // create the graphic for the feature + val graphic = feature?.let { createBufferGraphic(it) } + // add the graphic to the graphics overlay graphic?.let { graphicsOverlay.graphics.add(it) } // add the feature to the feature table viewModelScope.launch { - feature?.let { featureTable?.addFeature(it) } + feature?.let { featureTable!!.addFeature(it) } feature?.load()?.getOrElse { return@launch messageDialogVM.showMessageDialog("Error", it.message.toString()) } @@ -359,16 +335,11 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { */ fun clearFeature() { feature = null - _featureEditState.value = FeatureEditState(statusAttributes = statusAttributes()) + _featureEditState.value = FeatureEditState(statusAttributes = featureTable!!.statusFieldCodedValues()) } } -/** - * Enable status, maximum, and minimum values for the buffer size slider - */ -data class SliderControlParameters(val isEnabled: Boolean = false, val minRange: Int = 0, val maxRange: Int = 0) - /** * Currently selected status, protection, and buffer attributes for the feature under construction, * used to update the UI. @@ -379,5 +350,5 @@ data class FeatureEditState( val buffer: Int = 0, val statusAttributes: List = listOf(), val protectionAttributes: List = listOf(), - val sliderControlParameters: SliderControlParameters = SliderControlParameters() + val bufferRange: ContingentRangeValue? = null ) diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt index b12c643c5..ab3053b75 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt @@ -121,9 +121,9 @@ fun MainScreen(sampleName: String) { bottomSheetState, onBottomSheetStateChanged, featureEditState, - mapViewModel::onStatusAttributeSelect, - mapViewModel::onProtectionAttributeSelect, - mapViewModel::onBufferSizeSelect, + mapViewModel::onStatusAttributeSelected, + mapViewModel::onProtectionAttributeSelected, + mapViewModel::onBufferSizeSelected, mapViewModel::validateContingency ) } @@ -170,19 +170,31 @@ fun BottomSheetContents( Text(stringResource(R.string.attributes)) // dropdown boxes for selecting feature status/protection - AttributeDropdown(stringResource(R.string.status), featureEditState.status, featureEditState.statusAttributes, onStatusAttributeSelect) - AttributeDropdown(stringResource(R.string.protection), featureEditState.protection, featureEditState.protectionAttributes, onProtectionAttributeSelect) + AttributeDropdown( + attributeName = stringResource(R.string.status), + codedValue = featureEditState.status, + availableValues = featureEditState.statusAttributes, + onNewValueSelect = onStatusAttributeSelect + ) + AttributeDropdown( + attributeName = stringResource(R.string.protection), + codedValue = featureEditState.protection, + availableValues = featureEditState.protectionAttributes, + onNewValueSelect = onProtectionAttributeSelect) Spacer(Modifier.size(8.dp)) // buffer size displayed and updated in slider - var bufferSize by remember(key1= featureEditState) { mutableIntStateOf(featureEditState.buffer) } + var bufferSize by remember(key1 = featureEditState) { mutableIntStateOf(featureEditState.buffer) } + + val bufferRange = if (featureEditState.bufferRange != null) { + val min = featureEditState.bufferRange.minValue as Int + val max = featureEditState.bufferRange.maxValue as Int + min..max + } else {0..0} // recenter the slider if contingent values change - val minRange = featureEditState.sliderControlParameters.minRange - val maxRange = featureEditState.sliderControlParameters.maxRange - val bufferRange = minRange..maxRange - if (!bufferRange.contains(bufferSize)) { - bufferSize = (bufferRange.start + bufferRange.endInclusive) / 2 + if (!bufferRange.contains(bufferSize)){ + bufferSize = (bufferRange.first + bufferRange.last) /2 } Row { @@ -192,21 +204,22 @@ fun BottomSheetContents( } Slider( - enabled = featureEditState.sliderControlParameters.isEnabled, + enabled = bufferRange.first != bufferRange.last, value = bufferSize.toFloat(), - valueRange = minRange.toFloat()..maxRange.toFloat(), - steps = (bufferRange.endInclusive - bufferRange.start).toInt(), + valueRange = bufferRange.first.toFloat()..bufferRange.last.toFloat(), + steps = (bufferRange.last - bufferRange.first), onValueChange = { bufferSize = it.roundToInt() }, onValueChangeFinished = { onBufferSizeSelect(bufferSize) }, track = { sliderState -> SliderDefaults.Track( - enabled = featureEditState.sliderControlParameters.isEnabled, + enabled = bufferRange.first != bufferRange.last, sliderState = sliderState, drawStopIndicator = null, drawTick = { _, _ -> } ) } ) + HorizontalDivider() Text(stringResource(R.string.contingent_note)) Button( @@ -235,14 +248,14 @@ fun BottomSheetContents( fun AttributeDropdown( attributeName: String, codedValue: CodedValue?, - attributeOptions: List, + availableValues: List, onNewValueSelect: (CodedValue) -> Unit ) { var expanded by rememberSaveable { mutableStateOf(false) } ExposedDropdownMenuBox( modifier = Modifier.fillMaxWidth(), onExpandedChange = { - if (!attributeOptions.isEmpty()) { + if (availableValues.isNotEmpty()) { expanded = !expanded } }, @@ -252,7 +265,7 @@ fun AttributeDropdown( val textValue = codedValue?.name OutlinedTextField( - enabled = !attributeOptions.isEmpty(), + enabled = availableValues.isNotEmpty(), modifier = Modifier .fillMaxWidth() .menuAnchor(type = MenuAnchorType.PrimaryNotEditable), @@ -266,7 +279,7 @@ fun AttributeDropdown( expanded = expanded, onDismissRequest = { expanded = false }, ) { - attributeOptions.forEach { value -> + availableValues.forEach { value -> DropdownMenuItem( text = { Text(value.name) }, onClick = { diff --git a/samples/add-features-with-contingent-values/src/main/res/values/strings.xml b/samples/add-features-with-contingent-values/src/main/res/values/strings.xml index 7c7a0f282..c1f423bac 100644 --- a/samples/add-features-with-contingent-values/src/main/res/values/strings.xml +++ b/samples/add-features-with-contingent-values/src/main/res/values/strings.xml @@ -7,4 +7,6 @@ Status Protection Exclusion area buffer size: + No feature created yet + Input all values to add a feature to the map From 98ebd87ded44c7cec0367ddf457b27bde160fa1e Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Wed, 18 Dec 2024 11:15:12 +0000 Subject: [PATCH 06/13] Changes after PR review --- .../components/MapViewModel.kt | 81 +++++++++---------- .../screens/MainScreen.kt | 31 ++++--- 2 files changed, 55 insertions(+), 57 deletions(-) diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt index 586ada100..afda26472 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt @@ -113,14 +113,11 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } // get the contingent values definition from the feature table and load it - val contingentValuesDefinition = featureTable.contingentValuesDefinition - viewModelScope.launch { - contingentValuesDefinition.load().getOrElse { - messageDialogVM.showMessageDialog( - "Error", - "Error loading the contingent values definition" - ) - } + featureTable.contingentValuesDefinition.load().getOrElse { + messageDialogVM.showMessageDialog( + "Error", + "Error loading the contingent values definition" + ) } // create and load the feature layer from the feature table @@ -167,7 +164,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { /** * Creates buffer graphics for features in the [featureTable], and adds the graphics to - * the [graphicsOverlay] + * the [graphicsOverlay]. */ private suspend fun showBufferGraphics() { // clear the existing graphics @@ -178,25 +175,27 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { whereClause = "BufferSize > 0" } - // query the features using the queryParameters on the featureTable - val featureQueryResult = featureTable!!.queryFeatures(queryParameters).getOrNull() - val featureResultList = featureQueryResult?.toList() - - if (!featureResultList.isNullOrEmpty()) { - // create list of graphics for each query result - val graphics = featureResultList.map { createBufferGraphic(it) } - // add the graphics to the graphics overlay - graphicsOverlay.graphics.addAll(graphics) - } else { - messageDialogVM.showMessageDialog( - "Error", - "No features found with BufferSize > 0" - ) + featureTable?.let { + // query the features using the queryParameters on the featureTable + val featureQueryResult = it.queryFeatures(queryParameters).getOrNull() + val featureResultList = featureQueryResult?.toList() + + if (!featureResultList.isNullOrEmpty()) { + // create list of graphics for each query result + val graphics = featureResultList.map { createBufferGraphic(it) } + // add the graphics to the graphics overlay + graphicsOverlay.graphics.addAll(graphics) + } else { + messageDialogVM.showMessageDialog( + "Error", + "No features found with BufferSize > 0" + ) + } } } /** - * Creates and returns a graphic using the attributes of the given [feature] + * Creates and returns a graphic using the attributes of the given [feature]. */ private fun createBufferGraphic(feature: Feature): Graphic { // create the outline for the buffer symbol @@ -224,17 +223,19 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { return codedValueDomain.codedValues } - fun onStatusAttributeSelected(codedValue: CodedValue) { + fun onStatusAttributeSelected(codedValue: CodedValue) = featureTable?.let { featureTable -> viewModelScope.launch { - feature = featureTable!!.createFeature() as ArcGISFeature + feature = featureTable.createFeature() as ArcGISFeature feature?.attributes?.set("Status", codedValue.code) - _featureEditState.value = FeatureEditState( - status=codedValue, - statusAttributes = featureTable!!.statusFieldCodedValues(), - protectionAttributes = featureTable!!.protectionFieldCodedValues() - ) + featureTable.let { + _featureEditState.value = FeatureEditState( + status = codedValue, + statusAttributes = it.statusFieldCodedValues(), + protectionAttributes = it.protectionFieldCodedValues() + ) + } } } @@ -264,7 +265,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { feature?.attributes?.set("Protection", codedValue.code) _featureEditState.value = _featureEditState.value.copy( protection = codedValue, - bufferRange = featureTable!!.bufferRange(), + bufferRange = featureTable?.bufferRange() ) } @@ -295,7 +296,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { // validate the feature's contingencies val contingencyViolations = feature?.let { - featureTable!!.validateContingencyConstraints(it) + featureTable?.validateContingencyConstraints(it) } ?: return messageDialogVM.showMessageDialog(resources.getString(R.string.no_feature_created)) // if there are no contingency violations the feature is valid and ready to add to the feature table @@ -304,18 +305,15 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { // set the geometry of the feature to the map point feature?.geometry = mapPoint - // create the graphic for the feature - val graphic = feature?.let { createBufferGraphic(it) } + // create the buffer graphic for the feature + val bufferGraphic = feature?.let { createBufferGraphic(it) } // add the graphic to the graphics overlay - graphic?.let { graphicsOverlay.graphics.add(it) } + bufferGraphic?.let { graphicsOverlay.graphics.add(it) } // add the feature to the feature table viewModelScope.launch { - feature?.let { featureTable!!.addFeature(it) } - feature?.load()?.getOrElse { - return@launch messageDialogVM.showMessageDialog("Error", it.message.toString()) - } + feature?.let { featureTable?.addFeature(it) } } } else { val violations = contingencyViolations.joinToString(separator = "\n") { @@ -335,7 +333,8 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { */ fun clearFeature() { feature = null - _featureEditState.value = FeatureEditState(statusAttributes = featureTable!!.statusFieldCodedValues()) + _featureEditState.value = featureTable?.let {FeatureEditState(statusAttributes = it.statusFieldCodedValues())} + ?: return messageDialogVM.showMessageDialog("Feature Table not loaded") } } diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt index ab3053b75..e545fb464 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt @@ -110,16 +110,15 @@ fun MainScreen(sampleName: String) { mapViewModel.clearFeature() } ) { - val onBottomSheetStateChanged = { state: SheetState -> - if (!state.isVisible) { - showBottomSheet = false - mapViewModel.clearFeature() - } - } BottomSheetContents( mapPoint, bottomSheetState, - onBottomSheetStateChanged, + onBottomSheetStateChange = { state: SheetState -> + if (!state.isVisible) { + showBottomSheet = false + mapViewModel.clearFeature() + } + }, featureEditState, mapViewModel::onStatusAttributeSelected, mapViewModel::onProtectionAttributeSelected, @@ -151,9 +150,9 @@ fun BottomSheetContents( bottomSheetState: SheetState, onBottomSheetStateChange: (SheetState) -> Unit, featureEditState: FeatureEditState, - onStatusAttributeSelect: (CodedValue) -> Unit, - onProtectionAttributeSelect: (CodedValue) -> Unit, - onBufferSizeSelect: (Int) -> Unit, + onStatusAttributeSelected: (CodedValue) -> Unit, + onProtectionAttributeSelected: (CodedValue) -> Unit, + onBufferSizeSelected: (Int) -> Unit, onApplyButtonClicked: (Point) -> Unit, ) { val coroutineScope = rememberCoroutineScope() @@ -174,13 +173,13 @@ fun BottomSheetContents( attributeName = stringResource(R.string.status), codedValue = featureEditState.status, availableValues = featureEditState.statusAttributes, - onNewValueSelect = onStatusAttributeSelect + onNewValueSelected = onStatusAttributeSelected ) AttributeDropdown( attributeName = stringResource(R.string.protection), codedValue = featureEditState.protection, availableValues = featureEditState.protectionAttributes, - onNewValueSelect = onProtectionAttributeSelect) + onNewValueSelected = onProtectionAttributeSelected) Spacer(Modifier.size(8.dp)) // buffer size displayed and updated in slider @@ -209,7 +208,7 @@ fun BottomSheetContents( valueRange = bufferRange.first.toFloat()..bufferRange.last.toFloat(), steps = (bufferRange.last - bufferRange.first), onValueChange = { bufferSize = it.roundToInt() }, - onValueChangeFinished = { onBufferSizeSelect(bufferSize) }, + onValueChangeFinished = { onBufferSizeSelected(bufferSize) }, track = { sliderState -> SliderDefaults.Track( enabled = bufferRange.first != bufferRange.last, @@ -226,7 +225,7 @@ fun BottomSheetContents( modifier = Modifier.align(Alignment.CenterHorizontally), onClick = { // user may not have interacted with the slider - need to assign a value - onBufferSizeSelect(bufferSize) + onBufferSizeSelected(bufferSize) mapPoint?.let { onApplyButtonClicked(it) } @@ -249,7 +248,7 @@ fun AttributeDropdown( attributeName: String, codedValue: CodedValue?, availableValues: List, - onNewValueSelect: (CodedValue) -> Unit + onNewValueSelected: (CodedValue) -> Unit ) { var expanded by rememberSaveable { mutableStateOf(false) } ExposedDropdownMenuBox( @@ -284,7 +283,7 @@ fun AttributeDropdown( text = { Text(value.name) }, onClick = { expanded = false - onNewValueSelect(value) + onNewValueSelected(value) } ) } From 2ccdd6d4f39eb688394ae2abdb6efc4ac76dbf0b Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Thu, 19 Dec 2024 17:32:03 +0000 Subject: [PATCH 07/13] Changes after PR review --- .../components/MapViewModel.kt | 221 ++++++++++-------- .../screens/MainScreen.kt | 143 +++++++----- .../src/main/res/values/strings.xml | 4 +- 3 files changed, 209 insertions(+), 159 deletions(-) diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt index afda26472..a34965084 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt @@ -54,23 +54,23 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow class MapViewModel(application: Application) : AndroidViewModel(application) { - // Create an empty map, to be updated once data is loaded from the feature table - var arcGISMap by mutableStateOf(ArcGISMap()) - - // Create a message dialog view model for handling error messages - val messageDialogVM = MessageDialogViewModel() + private val provisionPath: String = application.getExternalFilesDir(null)?.path.toString() + + File.separator + application.getString(R.string.add_features_with_contingent_values_app_name) - // graphics overlay used to add feature graphics to the map - val graphicsOverlay = GraphicsOverlay() + private val cacheDir: File = application.cacheDir - // state flow of UI state + // flow of UI state private val _featureEditState = MutableStateFlow(FeatureEditState()) val featureEditState = _featureEditState.asStateFlow() - private val cacheDir: File = application.cacheDir + // create an empty map, to be updated once data is loaded from the feature table + var arcGISMap by mutableStateOf(ArcGISMap()) - private val provisionPath: String = application.getExternalFilesDir(null)?.path.toString() + - File.separator + application.getString(R.string.add_features_with_contingent_values_app_name) + // create a message dialog view model for handling error messages + val messageDialogVM = MessageDialogViewModel() + + // graphics overlay used to add feature graphics to the map + val graphicsOverlay = GraphicsOverlay() // offline vector tiled layer to be used as a basemap private val fillmoreVectorTileLayer = ArcGISVectorTiledLayer("$provisionPath/FillmoreTopographicMap.vtpk") @@ -84,6 +84,20 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { // instance of the feature table retrieved from the geodatabase, updates when new feature is added private var featureTable: ArcGISFeatureTable? = null + // create outline for the buffer symbol + private val lineSymbol = SimpleLineSymbol( + style = SimpleLineSymbolStyle.Solid, + color = Color.black, + width = 2f + ) + + // create the buffer symbol + private val bufferSymbol = SimpleFillSymbol( + style = SimpleFillSymbolStyle.ForwardDiagonal, + color = Color.red, + outline = lineSymbol + ) + init { // create a temporary directory for use with the geodatabase file createGeodatabaseCacheDir() @@ -92,31 +106,31 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { // retrieve and load the offline mobile geodatabase file from the cache directory geodatabase.load().getOrElse { messageDialogVM.showMessageDialog( - "Error loading GeoDatabase", - it.message.toString() + title = "Error loading GeoDatabase", + description = it.message.toString() ) } // get the first geodatabase feature table val featureTable = geodatabase.featureTables.firstOrNull() ?: return@launch messageDialogVM.showMessageDialog( - "Error", - "No feature table found in geodatabase" + title = "Error", + description = "No feature table found in geodatabase" ) // load the geodatabase feature table featureTable.load().getOrElse { return@launch messageDialogVM.showMessageDialog( - "Error loading feature table", - it.message.toString() + title = "Error loading feature table", + description = it.message.toString() ) } // get the contingent values definition from the feature table and load it featureTable.contingentValuesDefinition.load().getOrElse { messageDialogVM.showMessageDialog( - "Error", - "Error loading the contingent values definition" + title = "Error", + description = it.message.toString() ) } @@ -126,15 +140,15 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { // get the full extent of the feature layer val extent = featureLayer.fullExtent ?: return@launch messageDialogVM.showMessageDialog( - "Error", - "Error retrieving extent of the feature layer" + title = "Error", + description = "Error retrieving extent of the feature layer" ) // set the basemap to the offline vector tiled layer, and viewpoint to the feature layer extent arcGISMap = ArcGISMap(Basemap(fillmoreVectorTileLayer)).apply { initialViewpoint = Viewpoint(boundingGeometry = extent as Geometry) + operationalLayers.add(featureLayer) } - arcGISMap.operationalLayers.add(featureLayer) // keep the instance of the feature table this@MapViewModel.featureTable = featureTable @@ -187,8 +201,8 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { graphicsOverlay.graphics.addAll(graphics) } else { messageDialogVM.showMessageDialog( - "Error", - "No features found with BufferSize > 0" + title = "Error", + description = "No features found with BufferSize > 0" ) } } @@ -198,88 +212,46 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { * Creates and returns a graphic using the attributes of the given [feature]. */ private fun createBufferGraphic(feature: Feature): Graphic { - // create the outline for the buffer symbol - val lineSymbol = SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.black, 2f) - // create the buffer symbol - val bufferSymbol = - SimpleFillSymbol(SimpleFillSymbolStyle.ForwardDiagonal, Color.red, lineSymbol) // get the feature's buffer size val bufferSize = feature.attributes["BufferSize"] as Int // get a polygon using the feature's buffer size and geometry - val polygon = - feature.geometry?.let { GeometryEngine.bufferOrNull(it, bufferSize.toDouble()) } + val polygon = feature.geometry?.let { + GeometryEngine.bufferOrNull( + geometry = it, + distance = bufferSize.toDouble() + ) + } // create a graphic using the geometry and fill symbol - return Graphic(polygon, bufferSymbol) + return Graphic(geometry = polygon, symbol = bufferSymbol) } /** - * Retrieve the status fields, add the fields to a ContingentValueDomain. - * Used to display options in the UI. + * Create a new feature with the status attribute selected by the user. */ - private fun ArcGISFeatureTable.statusFieldCodedValues(): List { - val statusField = fields.find { field -> field.name == "Status" } - val codedValueDomain = statusField?.domain as CodedValueDomain - return codedValueDomain.codedValues - } - fun onStatusAttributeSelected(codedValue: CodedValue) = featureTable?.let { featureTable -> viewModelScope.launch { - feature = featureTable.createFeature() as ArcGISFeature - feature?.attributes?.set("Status", codedValue.code) + feature?.attributes?.set(key = "Status", value = codedValue.code) - featureTable.let { - _featureEditState.value = FeatureEditState( - status = codedValue, - statusAttributes = it.statusFieldCodedValues(), - protectionAttributes = it.protectionFieldCodedValues() - ) - } + _featureEditState.value = FeatureEditState( + selectedStatusAttribute = codedValue, + statusAttributes = featureTable.statusFieldCodedValues(), + protectionAttributes = featureTable.protectionFieldCodedValues() + ) } } - private fun ArcGISFeatureTable.protectionFieldCodedValues(): List { - // get the contingent value results with the feature for the protection field - val contingentValuesResult = feature?.let { - getContingentValuesOrNull(it, "Protection") - } - - // get the list of contingent values by field group - val contingentValues = contingentValuesResult?.byFieldGroup?.get("ProtectionFieldGroup") - - // convert the list of ContingentValues to a list of CodedValue - val protectionCodedValues: List = - contingentValues?.map { (it as ContingentCodedValue).codedValue } - ?: listOf().also { - messageDialogVM.showMessageDialog( - "Error", - "Error getting coded values by field group" - ) - } - - return protectionCodedValues - } - fun onProtectionAttributeSelected(codedValue: CodedValue) { - feature?.attributes?.set("Protection", codedValue.code) + feature?.attributes?.set(key = "Protection", value = codedValue.code) _featureEditState.value = _featureEditState.value.copy( - protection = codedValue, - bufferRange = featureTable?.bufferRange() + selectedProtectionAttribute = codedValue, + bufferRange = featureTable?.bufferRange().toIntRange() ) } - - private fun ArcGISFeatureTable.bufferRange(): ContingentRangeValue? { - val contingentValueResult = feature?.let { - getContingentValuesOrNull(it, "BufferSize") - } - - return contingentValueResult?.byFieldGroup?.get("BufferSizeFieldGroup")?.get(0) as? ContingentRangeValue - } - fun onBufferSizeSelected(bufferSize: Int) { - feature?.attributes?.set("BufferSize", bufferSize) - _featureEditState.value = _featureEditState.value.copy(buffer = bufferSize) + feature?.attributes?.set(key = "BufferSize", value = bufferSize) + _featureEditState.value = _featureEditState.value.copy(selectedBufferSize = bufferSize) } /** @@ -290,8 +262,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { val resources = getApplication().resources // check if all the features have been set if (featureTable == null) { - messageDialogVM.showMessageDialog(resources.getString(R.string.input_all_values)) - return + return messageDialogVM.showMessageDialog(resources.getString(R.string.input_all_values)) } // validate the feature's contingencies @@ -320,8 +291,8 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { violation -> violation.fieldGroup.name } messageDialogVM.showMessageDialog( - "Invalid contingent values", - "${contingencyViolations.size} violations found:\n" + violations + title = "Invalid contingent values", + description = "${contingencyViolations.size} violations found:\n" + violations ) } @@ -333,10 +304,70 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { */ fun clearFeature() { feature = null - _featureEditState.value = featureTable?.let {FeatureEditState(statusAttributes = it.statusFieldCodedValues())} - ?: return messageDialogVM.showMessageDialog("Feature Table not loaded") + _featureEditState.value = featureTable?.let { FeatureEditState(statusAttributes = it.statusFieldCodedValues()) } + ?: return + } + + /** + * Retrieves the possible status field values from the feature table, + * and add them to a ContingentValueDomain. + */ + private fun ArcGISFeatureTable.statusFieldCodedValues(): List { + val statusField = fields.find { field -> field.name == "Status" } + val codedValueDomain = statusField?.domain as CodedValueDomain + return codedValueDomain.codedValues } + /** + * Retrieves the possible protection field values from the feature table, contingent on the + * current status field value. + */ + private fun ArcGISFeatureTable.protectionFieldCodedValues(): List { + // get the contingent value results with the feature for the protection field + val contingentValuesResult = feature?.let { + getContingentValuesOrNull(feature = it, field = "Protection") + } + + // get the list of contingent values by field group + val contingentValues = contingentValuesResult?.byFieldGroup?.get("ProtectionFieldGroup") + + // convert the list of ContingentValues to a list of CodedValue + val protectionCodedValues: List = + contingentValues?.map { (it as ContingentCodedValue).codedValue } + ?: listOf().also { + messageDialogVM.showMessageDialog( + title = "Error", + description = "Error getting coded values by field group" + ) + } + + return protectionCodedValues + } + + /** + * Retrieves the buffer size from the feature table, contingent on the status and protection field values. + */ + private fun ArcGISFeatureTable.bufferRange(): ContingentRangeValue? { + val contingentValueResult = feature?.let { + getContingentValuesOrNull(it, "BufferSize") + } + + return contingentValueResult?.byFieldGroup?.get("BufferSizeFieldGroup")?.get(0) as? ContingentRangeValue + } + + /** + * Converts this [ContingentRangeValue] to an [IntRange]. + */ + private fun ContingentRangeValue?.toIntRange() : IntRange { + val bufferRange = if (this != null) { + val min = this.minValue as Int + val max = this.maxValue as Int + min..max + } else { + 0..0 + } + return bufferRange + } } /** @@ -344,10 +375,10 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { * used to update the UI. */ data class FeatureEditState( - val status: CodedValue? = null, - val protection: CodedValue? = null, - val buffer: Int = 0, + val selectedStatusAttribute: CodedValue? = null, val statusAttributes: List = listOf(), + val selectedProtectionAttribute: CodedValue? = null, val protectionAttributes: List = listOf(), - val bufferRange: ContingentRangeValue? = null + val selectedBufferSize: Int = 0, + val bufferRange: IntRange = 0..0 ) diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt index e545fb464..500c7b26e 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt @@ -16,6 +16,8 @@ package com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.screens +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -25,6 +27,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -35,23 +39,24 @@ import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold -import androidx.compose.material3.SheetState import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arcgismaps.data.CodedValue @@ -62,8 +67,8 @@ import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.Map import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.FeatureEditState +import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme import kotlin.math.roundToInt -import kotlinx.coroutines.launch /** * Main screen layout for the sample app @@ -71,6 +76,9 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen(sampleName: String) { + var showBottomSheet by remember { mutableStateOf(false) } + val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + // create a ViewModel to handle MapView interactions val mapViewModel: MapViewModel = viewModel() @@ -78,13 +86,20 @@ fun MainScreen(sampleName: String) { val featureEditState by mapViewModel.featureEditState.collectAsStateWithLifecycle() // point on map tapped by user - var mapPoint by remember { mutableStateOf(null) } + var selectedPoint by remember { mutableStateOf(null) } + + LaunchedEffect(showBottomSheet) { + if (showBottomSheet) { + bottomSheetState.show() + } else { + bottomSheetState.hide() + mapViewModel.clearFeature() + } + } Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { - var showBottomSheet by remember { mutableStateOf(false) } - val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) Box { MapView( @@ -93,11 +108,9 @@ fun MainScreen(sampleName: String) { .padding(it), arcGISMap = mapViewModel.arcGISMap, graphicsOverlays = listOf(mapViewModel.graphicsOverlay), - onSingleTapConfirmed = { - it.mapPoint.let { point -> - mapPoint = point - showBottomSheet = true - } + onSingleTapConfirmed = { tapEvent -> + selectedPoint = tapEvent.mapPoint + showBottomSheet = true } ) @@ -105,25 +118,19 @@ fun MainScreen(sampleName: String) { ModalBottomSheet( modifier = Modifier.wrapContentHeight(), sheetState = bottomSheetState, - onDismissRequest = { - showBottomSheet = false - mapViewModel.clearFeature() - } + onDismissRequest = { showBottomSheet = false } ) { BottomSheetContents( - mapPoint, - bottomSheetState, - onBottomSheetStateChange = { state: SheetState -> - if (!state.isVisible) { + featureEditState = featureEditState, + onStatusAttributeSelected = mapViewModel::onStatusAttributeSelected, + onProtectionAttributeSelected = mapViewModel::onProtectionAttributeSelected, + onBufferSizeSelected = mapViewModel::onBufferSizeSelected, + onApplyButtonClicked = { + selectedPoint?.let { point -> + mapViewModel.validateContingency(point) showBottomSheet = false - mapViewModel.clearFeature() } - }, - featureEditState, - mapViewModel::onStatusAttributeSelected, - mapViewModel::onProtectionAttributeSelected, - mapViewModel::onBufferSizeSelected, - mapViewModel::validateContingency + } ) } } @@ -146,63 +153,66 @@ fun MainScreen(sampleName: String) { @OptIn(ExperimentalMaterial3Api::class) @Composable fun BottomSheetContents( - mapPoint: Point?, - bottomSheetState: SheetState, - onBottomSheetStateChange: (SheetState) -> Unit, featureEditState: FeatureEditState, onStatusAttributeSelected: (CodedValue) -> Unit, onProtectionAttributeSelected: (CodedValue) -> Unit, onBufferSizeSelected: (Int) -> Unit, - onApplyButtonClicked: (Point) -> Unit, + onApplyButtonClicked: () -> Unit, ) { - val coroutineScope = rememberCoroutineScope() Column( - modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp), + modifier = Modifier + .padding(24.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( text = stringResource(R.string.add_feature), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleLarge, modifier = Modifier.align(Alignment.CenterHorizontally) ) Spacer(Modifier.size(8.dp)) - Text(stringResource(R.string.attributes)) + Text( + text = stringResource(R.string.attributes), + style = MaterialTheme.typography.labelLarge + ) // dropdown boxes for selecting feature status/protection AttributeDropdown( attributeName = stringResource(R.string.status), - codedValue = featureEditState.status, + codedValue = featureEditState.selectedStatusAttribute, availableValues = featureEditState.statusAttributes, onNewValueSelected = onStatusAttributeSelected ) AttributeDropdown( attributeName = stringResource(R.string.protection), - codedValue = featureEditState.protection, + codedValue = featureEditState.selectedProtectionAttribute, availableValues = featureEditState.protectionAttributes, onNewValueSelected = onProtectionAttributeSelected) Spacer(Modifier.size(8.dp)) // buffer size displayed and updated in slider - var bufferSize by remember(key1 = featureEditState) { mutableIntStateOf(featureEditState.buffer) } - - val bufferRange = if (featureEditState.bufferRange != null) { - val min = featureEditState.bufferRange.minValue as Int - val max = featureEditState.bufferRange.maxValue as Int - min..max - } else {0..0} + var bufferSize by remember(key1 = featureEditState) { mutableIntStateOf(featureEditState.selectedBufferSize) } + val bufferRange by remember (key1 = featureEditState ) { mutableStateOf(featureEditState.bufferRange) } - // recenter the slider if contingent values change - if (!bufferRange.contains(bufferSize)){ - bufferSize = (bufferRange.first + bufferRange.last) /2 + // update the slider if contingent values change + if (!bufferRange.contains(featureEditState.selectedBufferSize)) { + onBufferSizeSelected((bufferRange.first + bufferRange.last) / 2) + } else if (bufferRange.first == bufferRange.last) { + onBufferSizeSelected(bufferRange.first) } Row { - Text(stringResource(R.string.exclusion_area_buffer_size)) + Text( + text = stringResource(R.string.exclusion_area_buffer_size), + style = MaterialTheme.typography.labelLarge + ) Spacer(Modifier.weight(1f)) Text(text = if (bufferSize > 0) bufferSize.toString() else "") } Slider( + modifier = Modifier.padding(horizontal = 12.dp), enabled = bufferRange.first != bufferRange.last, value = bufferSize.toFloat(), valueRange = bufferRange.first.toFloat()..bufferRange.last.toFloat(), @@ -220,21 +230,13 @@ fun BottomSheetContents( ) HorizontalDivider() - Text(stringResource(R.string.contingent_note)) + Text( + text = stringResource(R.string.contingent_note), + style = MaterialTheme.typography.labelLarge + ) Button( modifier = Modifier.align(Alignment.CenterHorizontally), - onClick = { - // user may not have interacted with the slider - need to assign a value - onBufferSizeSelected(bufferSize) - mapPoint?.let { - onApplyButtonClicked(it) - } - coroutineScope.launch { - bottomSheetState.hide() - }.invokeOnCompletion { - onBottomSheetStateChange(bottomSheetState) - } - } + onClick = onApplyButtonClicked ) { Text(stringResource(R.string.apply)) } @@ -270,13 +272,13 @@ fun AttributeDropdown( .menuAnchor(type = MenuAnchorType.PrimaryNotEditable), value = textValue ?: "", onValueChange = {}, - label = { Text("Select $attributeName Attribute") }, + label = { Text(attributeName) }, readOnly = true ) ExposedDropdownMenu( expanded = expanded, - onDismissRequest = { expanded = false }, + onDismissRequest = { expanded = false } ) { availableValues.forEach { value -> DropdownMenuItem( @@ -290,3 +292,20 @@ fun AttributeDropdown( } } } + +@Preview(showBackground = true) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true) +@Composable +fun SheetPreview() { + SampleAppTheme { + Surface { + BottomSheetContents( + featureEditState = FeatureEditState(), + onStatusAttributeSelected = { }, + onProtectionAttributeSelected = { }, + onBufferSizeSelected = { }, + onApplyButtonClicked = { }, + ) + } + } +} diff --git a/samples/add-features-with-contingent-values/src/main/res/values/strings.xml b/samples/add-features-with-contingent-values/src/main/res/values/strings.xml index c1f423bac..c3a9f65a6 100644 --- a/samples/add-features-with-contingent-values/src/main/res/values/strings.xml +++ b/samples/add-features-with-contingent-values/src/main/res/values/strings.xml @@ -1,9 +1,9 @@ Add features with contingent values The options will vary depending on which values are selected. - Add Feature + Add Bird Nest Apply - Attributes: + Set the attributes: Status Protection Exclusion area buffer size: From 296ffef0860dd9a34623be059f8d601cd112ff63 Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Fri, 3 Jan 2025 15:31:41 +0000 Subject: [PATCH 08/13] Setup geodatabase after it's copied into cache --- .../components/MapViewModel.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt index a34965084..9d62f6c56 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt @@ -75,9 +75,6 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { // offline vector tiled layer to be used as a basemap private val fillmoreVectorTileLayer = ArcGISVectorTiledLayer("$provisionPath/FillmoreTopographicMap.vtpk") - // mobile database containing offline feature data - private val geodatabase = Geodatabase("${cacheDir.path}/ContingentValuesBirdNests.geodatabase") - // instance of the contingent feature to be added to the map private var feature: ArcGISFeature? = null @@ -102,6 +99,9 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { // create a temporary directory for use with the geodatabase file createGeodatabaseCacheDir() + // create mobile database containing offline feature data + val geodatabase = Geodatabase("${cacheDir.path}/ContingentValuesBirdNests.geodatabase") + viewModelScope.launch { // retrieve and load the offline mobile geodatabase file from the cache directory geodatabase.load().getOrElse { @@ -163,8 +163,8 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } /** - * Geodatabase creates and uses various temporary files while processing a database, - * which will need to be cleared before looking up the [geodatabase] again. + * [Geodatabase] creates and uses various temporary files while processing a database, + * which will need to be cleared before looking up the geodatabase again. * A copy of the original geodatabase file is created in the cache folder. */ private fun createGeodatabaseCacheDir() { From 203ee04b11e8276e2df9408eb31dcaad346e4d9b Mon Sep 17 00:00:00 2001 From: Gunther Heppner Date: Tue, 7 Jan 2025 15:50:14 +0100 Subject: [PATCH 09/13] close the geodatabase on exit from the sample --- .../components/MapViewModel.kt | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt index 9d62f6c56..4983113b8 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt @@ -31,6 +31,7 @@ import com.arcgismaps.data.ContingentCodedValue import com.arcgismaps.data.ContingentRangeValue import com.arcgismaps.data.Feature import com.arcgismaps.data.Geodatabase +import com.arcgismaps.data.GeodatabaseFeatureTable import com.arcgismaps.data.QueryParameters import com.arcgismaps.geometry.Geometry import com.arcgismaps.geometry.GeometryEngine @@ -81,6 +82,9 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { // instance of the feature table retrieved from the geodatabase, updates when new feature is added private var featureTable: ArcGISFeatureTable? = null + // feature layer to be added to the map, based on the feature table retrieved from the geodatabase + private var featureLayer: FeatureLayer? = null + // create outline for the buffer symbol private val lineSymbol = SimpleLineSymbol( style = SimpleLineSymbolStyle.Solid, @@ -135,19 +139,19 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } // create and load the feature layer from the feature table - val featureLayer = FeatureLayer.createWithFeatureTable(featureTable) - - // get the full extent of the feature layer - val extent = featureLayer.fullExtent - ?: return@launch messageDialogVM.showMessageDialog( - title = "Error", - description = "Error retrieving extent of the feature layer" - ) + featureLayer = FeatureLayer.createWithFeatureTable(featureTable).also { + // get the full extent of the feature layer + val extent = it.fullExtent + ?: return@launch messageDialogVM.showMessageDialog( + title = "Error", + description = "Error retrieving extent of the feature layer" + ) - // set the basemap to the offline vector tiled layer, and viewpoint to the feature layer extent - arcGISMap = ArcGISMap(Basemap(fillmoreVectorTileLayer)).apply { - initialViewpoint = Viewpoint(boundingGeometry = extent as Geometry) - operationalLayers.add(featureLayer) + // set the basemap to the offline vector tiled layer, and viewpoint to the feature layer extent + arcGISMap = ArcGISMap(Basemap(fillmoreVectorTileLayer)).apply { + initialViewpoint = Viewpoint(boundingGeometry = extent as Geometry) + operationalLayers.add(it) + } } // keep the instance of the feature table @@ -162,6 +166,16 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } + override fun onCleared() { + super.onCleared() + + // remove the feature layer from the map in order to safely close the underlying geodatabase + featureLayer?.let { + arcGISMap.operationalLayers.remove(it) + (it.featureTable as GeodatabaseFeatureTable).geodatabase?.close() + } + } + /** * [Geodatabase] creates and uses various temporary files while processing a database, * which will need to be cleared before looking up the geodatabase again. From aa896c6a5fff61ad1724d2da831c7d28ea6691e6 Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Wed, 8 Jan 2025 17:27:39 +0000 Subject: [PATCH 10/13] Remove unnecessary FeatureLayer removal --- .../components/MapViewModel.kt | 7 ++----- .../addfeatureswithcontingentvalues/screens/MainScreen.kt | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt index 4983113b8..c94e4aaf8 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt @@ -169,11 +169,8 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { override fun onCleared() { super.onCleared() - // remove the feature layer from the map in order to safely close the underlying geodatabase - featureLayer?.let { - arcGISMap.operationalLayers.remove(it) - (it.featureTable as GeodatabaseFeatureTable).geodatabase?.close() - } + // close the geodatabase to ensure cleanup of temporary files + (featureLayer?.featureTable as GeodatabaseFeatureTable).geodatabase?.close() } /** diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt index 500c7b26e..9696ee48f 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt @@ -49,7 +49,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.rememberSaveable @@ -59,6 +58,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import com.arcgismaps.data.CodedValue import com.arcgismaps.geometry.Point import com.arcgismaps.toolkit.geoviewcompose.MapView From 6caac75e98ca9e9fa4eeccb8cc14fb0cb85ef8ae Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Wed, 8 Jan 2025 17:39:46 +0000 Subject: [PATCH 11/13] Add comment re: geodatabase closure --- samples/add-features-with-contingent-values/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/add-features-with-contingent-values/README.md b/samples/add-features-with-contingent-values/README.md index 269fdc4be..8d4935142 100644 --- a/samples/add-features-with-contingent-values/README.md +++ b/samples/add-features-with-contingent-values/README.md @@ -29,6 +29,7 @@ Tap on the map to add a feature symbolizing a bird's nest. Then choose values de ii. Get an array of valid `ContingentValues` from `ContingentValuesResult.contingentValuesByFieldGroup` dictionary with the name of the relevant field group. iii. Iterate through the array of valid contingent values to create an array of `ContingentCodedValue` names or the minimum and maximum values of a `ContingentRangeValue` depending on the type of `ContingentValue` returned. 10. Validate the feature's contingent values by using `validateContingencyConstraints(feature)` with the current feature. If the resulting array is empty, the selected values are valid. +11. Close the geodatabase once operations are complete to ensure temporary files are cleaned up. ## Relevant API From 68910dfdeae636ff23ea4875e3c5ace8d6efd5fe Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Thu, 9 Jan 2025 09:44:20 +0000 Subject: [PATCH 12/13] Remove redirect --- .../add-features-with-contingent-values/README.metadata.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/samples/add-features-with-contingent-values/README.metadata.json b/samples/add-features-with-contingent-values/README.metadata.json index 6e7b23a8d..e5cca935d 100644 --- a/samples/add-features-with-contingent-values/README.metadata.json +++ b/samples/add-features-with-contingent-values/README.metadata.json @@ -25,9 +25,7 @@ "ContingentValuesResult" ], "language": "kotlin", - "redirect_from": [ - "/android/latest/sample-code/add-features-with-contingent-values.htm" - ], + "redirect_from": [], "relevant_apis": [ "ArcGISFeatureTable", "CodedValue", From bdc5b5526dc76b210e841c8a26dc943f5b7ad8cf Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Thu, 9 Jan 2025 10:01:11 +0000 Subject: [PATCH 13/13] Renames for IDE discoverability --- .../README.metadata.json | 4 ++-- .../addfeatureswithcontingentvalues/MainActivity.kt | 8 ++++---- ...del.kt => AddFeaturesWithContingentValuesViewModel.kt} | 4 ++-- ...Screen.kt => AddFeaturesWithContingentValuesScreen.kt} | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) rename samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/{MapViewModel.kt => AddFeaturesWithContingentValuesViewModel.kt} (98%) rename samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/{MainScreen.kt => AddFeaturesWithContingentValuesScreen.kt} (98%) diff --git a/samples/add-features-with-contingent-values/README.metadata.json b/samples/add-features-with-contingent-values/README.metadata.json index e5cca935d..95e76e707 100644 --- a/samples/add-features-with-contingent-values/README.metadata.json +++ b/samples/add-features-with-contingent-values/README.metadata.json @@ -37,10 +37,10 @@ "ContingentValuesResult" ], "snippets": [ - "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt", + "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/AddFeaturesWithContingentValuesViewModel.kt", "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/DownloadActivity.kt", "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/MainActivity.kt", - "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt" + "src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/AddFeaturesWithContingentValuesScreen.kt" ], "title": "Add features with contingent values" } diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/MainActivity.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/MainActivity.kt index 9e81a9fe4..697ca7c7e 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/MainActivity.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/MainActivity.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Composable import com.arcgismaps.ApiKey import com.arcgismaps.ArcGISEnvironment import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme -import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.screens.MainScreen +import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.screens.AddFeaturesWithContingentValuesScreen class MainActivity : ComponentActivity() { @@ -37,17 +37,17 @@ class MainActivity : ComponentActivity() { setContent { SampleAppTheme { - SampleApp() + AddFeaturesWithContingentValuesApp() } } } @Composable - private fun SampleApp() { + private fun AddFeaturesWithContingentValuesApp() { Surface( color = MaterialTheme.colorScheme.background ) { - MainScreen( + AddFeaturesWithContingentValuesScreen( sampleName = getString(R.string.add_features_with_contingent_values_app_name) ) } diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/AddFeaturesWithContingentValuesViewModel.kt similarity index 98% rename from samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt rename to samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/AddFeaturesWithContingentValuesViewModel.kt index c94e4aaf8..fc508d17f 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/MapViewModel.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/components/AddFeaturesWithContingentValuesViewModel.kt @@ -54,7 +54,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -class MapViewModel(application: Application) : AndroidViewModel(application) { +class AddFeaturesWithContingentValuesViewModel(application: Application) : AndroidViewModel(application) { private val provisionPath: String = application.getExternalFilesDir(null)?.path.toString() + File.separator + application.getString(R.string.add_features_with_contingent_values_app_name) @@ -155,7 +155,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } // keep the instance of the feature table - this@MapViewModel.featureTable = featureTable + this@AddFeaturesWithContingentValuesViewModel.featureTable = featureTable // add buffer graphics for the feature layer showBufferGraphics() diff --git a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/AddFeaturesWithContingentValuesScreen.kt similarity index 98% rename from samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt rename to samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/AddFeaturesWithContingentValuesScreen.kt index 9696ee48f..d46ca167c 100644 --- a/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/MainScreen.kt +++ b/samples/add-features-with-contingent-values/src/main/java/com/esri/arcgismaps/sample/addfeatureswithcontingentvalues/screens/AddFeaturesWithContingentValuesScreen.kt @@ -63,7 +63,7 @@ import com.arcgismaps.data.CodedValue import com.arcgismaps.geometry.Point import com.arcgismaps.toolkit.geoviewcompose.MapView import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.R -import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.MapViewModel +import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.AddFeaturesWithContingentValuesViewModel import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar import com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.FeatureEditState @@ -75,12 +75,12 @@ import kotlin.math.roundToInt */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MainScreen(sampleName: String) { +fun AddFeaturesWithContingentValuesScreen(sampleName: String) { var showBottomSheet by remember { mutableStateOf(false) } val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) // create a ViewModel to handle MapView interactions - val mapViewModel: MapViewModel = viewModel() + val mapViewModel: AddFeaturesWithContingentValuesViewModel = viewModel() // flows from view model for displaying state in UI val featureEditState by mapViewModel.featureEditState.collectAsStateWithLifecycle()