Create and add features whose attribute values satisfy a predefined set of contingencies.

Use case
Contingent values are a data design feature that allow you to make values in one field dependent on values in another field. Your choice for a value on one field further constrains the domain values that can be placed on another field. In this way, contingent values enforce data integrity by applying additional constraints to reduce the number of valid field inputs.
For example, a field crew working in a sensitive habitat area may be required to stay a certain distance away from occupied bird nests, but the size of that exclusion area differs depending on the bird’s level of protection according to presiding laws. Surveyors can add points of bird nests in the work area and their selection of the size of the exclusion area will be contingent on the values in other attribute fields.
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 “Apply” to add the feature to the map.
How it works
- Create and load the
Geodatabasefrom the mobile geodatabase location on file. - Load the first
GeodatabaseFeatureTable. - Load the
ContingentValuesDefinitionfrom the feature table. - Create a new
FeatureLayerfrom the feature table and add it to the map. - Create a new
ArcGISFeatureusingGeodatabaseFeatureTable.createFeature() - Get the first field by name using
ArcGISFeatureTable.fields.find{ }. - Then get the
Field.domainas anCodedValueDomain. - Get the coded value domain’s
codedValuesto get an array ofCodedValue’s. - After selecting a value from the initial coded values for the first field, retrieve the remaining valid contingent values for each field as you select the values for the attributes.
i. Get theContingentValueResults by usingArcGISFeatureTable.getContingentValues(ArcGISFeature, "field_name")with the feature and the target field by name.
ii. Get an array of validContingentValuesfromContingentValuesResult.contingentValuesByFieldGroupdictionary with the name of the relevant field group.
iii. Iterate through the array of valid contingent values to create an array ofContingentCodedValuenames or the minimum and maximum values of aContingentRangeValuedepending on the type ofContingentValuereturned. - 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. - Close the geodatabase once operations are complete to ensure temporary files are cleaned up.
Relevant API
- ArcGISFeatureTable
- CodedValue
- CodedValueDomain
- ContingencyConstraintViolation
- ContingentCodedValue
- ContingentRangeValue
- ContingentValuesDefinition
- ContingentValuesResult
About the data
This sample uses the Contingent values birds nests mobile geodatabase and the Fillmore topographic map vector tile package for the basemap. The mobile geodatabase contains birds nests in the Fillmore area, defined with contingent values. Each feature contains information about its status, protection, and buffer size.
Additional information
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.
Tags
coded values, compose, contingent values, feature table, geodatabase, geoview, mapview, toolkit
Sample Code
/* 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
import android.content.Intentimport android.os.Bundleimport com.esri.arcgismaps.sample.sampleslib.DownloaderActivity
class DownloadActivity : DownloaderActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) downloadAndStartSample( Intent(this, MainActivity::class.java), // get the app name of the sample getString(R.string.add_features_with_contingent_values_app_name), listOf( // Geodatabase containing bird nests defined with contingent values "https://www.arcgis.com/home/item.html?id=e12b54ea799f4606a2712157cf9f6e41", // Vector tile package of the Fillmore area "https://www.arcgis.com/home/item.html?id=b5106355f1634b8996e634c04b6a930a" ) ) }}/* 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
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.screens.AddFeaturesWithContingentValuesScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // authentication with an API key or named user is // required to access basemaps and other location services ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
enableEdgeToEdge() setContent { SampleAppTheme { AddFeaturesWithContingentValuesApp() } } }
@Composable private fun AddFeaturesWithContingentValuesApp() { Surface( color = MaterialTheme.colorScheme.background ) { AddFeaturesWithContingentValuesScreen( sampleName = getString(R.string.add_features_with_contingent_values_app_name) ) } }}/* 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.Applicationimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.Colorimport com.arcgismaps.data.ArcGISFeatureimport com.arcgismaps.data.ArcGISFeatureTableimport com.arcgismaps.data.CodedValueimport com.arcgismaps.data.CodedValueDomainimport com.arcgismaps.data.ContingentCodedValueimport com.arcgismaps.data.ContingentRangeValueimport com.arcgismaps.data.Featureimport com.arcgismaps.data.Geodatabaseimport com.arcgismaps.data.GeodatabaseFeatureTableimport com.arcgismaps.data.QueryParametersimport com.arcgismaps.geometry.Geometryimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.Basemapimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.layers.ArcGISVectorTiledLayerimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.symbology.SimpleFillSymbolimport com.arcgismaps.mapping.symbology.SimpleFillSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.Rimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport java.io.Fileimport kotlinx.coroutines.launchimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asStateFlow
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)
private val cacheDir: File = application.cacheDir
// flow of UI state private val _featureEditState = MutableStateFlow(FeatureEditState()) val featureEditState = _featureEditState.asStateFlow()
// 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()
// offline vector tiled layer to be used as a basemap private val fillmoreVectorTileLayer = ArcGISVectorTiledLayer("$provisionPath/FillmoreTopographicMap.vtpk")
// instance of the contingent feature to be added to the map 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
// 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, 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()
// 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 { messageDialogVM.showMessageDialog( title = "Error loading GeoDatabase", description = it.message.toString() ) }
// get the first geodatabase feature table val featureTable = geodatabase.featureTables.firstOrNull() ?: return@launch messageDialogVM.showMessageDialog( title = "Error", description = "No feature table found in geodatabase" )
// load the geodatabase feature table featureTable.load().getOrElse { return@launch messageDialogVM.showMessageDialog( 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( title = "Error", description = it.message.toString() ) }
// create and load the feature layer from the feature table 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(it) } }
// keep the instance of the feature table this@AddFeaturesWithContingentValuesViewModel.featureTable = featureTable
// add buffer graphics for the feature layer showBufferGraphics()
// get status attributes for new features _featureEditState.value = _featureEditState.value.copy(statusAttributes = featureTable.statusFieldCodedValues()) }
}
override fun onCleared() { super.onCleared()
// close the geodatabase to ensure cleanup of temporary files (featureLayer?.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. * A copy of the original geodatabase file is created in the cache folder. */ private 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") ) }
/** * Creates buffer graphics for features in the [featureTable], and adds the graphics to * the [graphicsOverlay]. */ private suspend fun showBufferGraphics() { // clear the existing graphics graphicsOverlay.graphics.clear()
// create buffer graphics for features which need them val queryParameters = QueryParameters().apply { whereClause = "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( title = "Error", description = "No features found with BufferSize > 0" ) } } }
/** * Creates and returns a graphic using the attributes of the given [feature]. */ private fun createBufferGraphic(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( geometry = it, distance = bufferSize.toDouble() ) }
// create a graphic using the geometry and fill symbol return Graphic(geometry = polygon, symbol = bufferSymbol) }
/** * Create a new feature with the status attribute selected by the user. */ fun onStatusAttributeSelected(codedValue: CodedValue) = featureTable?.let { featureTable -> viewModelScope.launch { feature = featureTable.createFeature() as ArcGISFeature feature?.attributes?.set(key = "Status", value = codedValue.code)
_featureEditState.value = FeatureEditState( selectedStatusAttribute = codedValue, statusAttributes = featureTable.statusFieldCodedValues(), protectionAttributes = featureTable.protectionFieldCodedValues() ) } }
fun onProtectionAttributeSelected(codedValue: CodedValue) { feature?.attributes?.set(key = "Protection", value = codedValue.code) _featureEditState.value = _featureEditState.value.copy( selectedProtectionAttribute = codedValue, bufferRange = featureTable?.bufferRange().toIntRange() ) } fun onBufferSizeSelected(bufferSize: Int) { feature?.attributes?.set(key = "BufferSize", value = bufferSize) _featureEditState.value = _featureEditState.value.copy(selectedBufferSize = bufferSize) }
/** * Ensure that the selected values are a valid combination. * If contingencies are valid, then display [feature] on the [mapPoint] */ fun validateContingency(mapPoint: Point) { val resources = getApplication<Application>().resources // check if all the features have been set if (featureTable == null) { return messageDialogVM.showMessageDialog(resources.getString(R.string.input_all_values)) }
// validate the feature's contingencies val contingencyViolations = feature?.let { 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 if (contingencyViolations.isEmpty()) {
// set the geometry of the feature to the map point feature?.geometry = mapPoint
// create the buffer graphic for the feature val bufferGraphic = feature?.let { createBufferGraphic(it) }
// add the graphic to the graphics overlay bufferGraphic?.let { graphicsOverlay.graphics.add(it) }
// add the feature to the feature table viewModelScope.launch { feature?.let { featureTable?.addFeature(it) } } } else { val violations = contingencyViolations.joinToString(separator = "\n") { violation -> violation.fieldGroup.name } messageDialogVM.showMessageDialog( title = "Invalid contingent values", description = "${contingencyViolations.size} violations found:\n" + violations ) }
}
/** * 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 _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<CodedValue> { 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<CodedValue> { // 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<CodedValue> = contingentValues?.map { (it as ContingentCodedValue).codedValue } ?: listOf<CodedValue>().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 }}
/** * Currently selected status, protection, and buffer attributes for the feature under construction, * used to update the UI. */data class FeatureEditState( val selectedStatusAttribute: CodedValue? = null, val statusAttributes: List<CodedValue> = listOf(), val selectedProtectionAttribute: CodedValue? = null, val protectionAttributes: List<CodedValue> = listOf(), val selectedBufferSize: Int = 0, val bufferRange: IntRange = 0..0)/* 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 android.content.res.Configurationimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.Spacerimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.foundation.layout.wrapContentHeightimport androidx.compose.foundation.rememberScrollStateimport androidx.compose.foundation.verticalScrollimport androidx.compose.material3.Buttonimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuAnchorTypeimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.HorizontalDividerimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.ModalBottomSheetimport androidx.compose.material3.OutlinedTextFieldimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.SliderDefaultsimport androidx.compose.material3.Surfaceimport androidx.compose.material3.Textimport androidx.compose.material3.rememberModalBottomSheetStateimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.ui.Modifierimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.saveable.rememberSaveableimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.tooling.preview.Previewimport androidx.compose.ui.unit.dpimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.data.CodedValueimport com.arcgismaps.geometry.Pointimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.Rimport com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.AddFeaturesWithContingentValuesViewModelimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.addfeatureswithcontingentvalues.components.FeatureEditStateimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport kotlin.math.roundToInt
/** * Main screen layout for the sample app */@OptIn(ExperimentalMaterial3Api::class)@Composablefun AddFeaturesWithContingentValuesScreen(sampleName: String) { var showBottomSheet by remember { mutableStateOf(false) } val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
// create a ViewModel to handle MapView interactions val mapViewModel: AddFeaturesWithContingentValuesViewModel = viewModel()
// flows from view model for displaying state in UI val featureEditState by mapViewModel.featureEditState.collectAsStateWithLifecycle()
// point on map tapped by user var selectedPoint by remember { mutableStateOf<Point?>(null) }
LaunchedEffect(showBottomSheet) { if (showBottomSheet) { bottomSheetState.show() } else { bottomSheetState.hide() mapViewModel.clearFeature() } }
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = {
Box { MapView( modifier = Modifier .fillMaxSize() .padding(it), arcGISMap = mapViewModel.arcGISMap, graphicsOverlays = listOf(mapViewModel.graphicsOverlay), onSingleTapConfirmed = { tapEvent -> selectedPoint = tapEvent.mapPoint showBottomSheet = true } )
if (showBottomSheet) { ModalBottomSheet( modifier = Modifier.wrapContentHeight(), sheetState = bottomSheetState, onDismissRequest = { showBottomSheet = false } ) { BottomSheetContents( featureEditState = featureEditState, onStatusAttributeSelected = mapViewModel::onStatusAttributeSelected, onProtectionAttributeSelected = mapViewModel::onProtectionAttributeSelected, onBufferSizeSelected = mapViewModel::onBufferSizeSelected, onApplyButtonClicked = { selectedPoint?.let { point -> mapViewModel.validateContingency(point) showBottomSheet = false } } ) } } }
// message dialog view model for displaying error messages mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}
@OptIn(ExperimentalMaterial3Api::class)@Composablefun BottomSheetContents( featureEditState: FeatureEditState, onStatusAttributeSelected: (CodedValue) -> Unit, onProtectionAttributeSelected: (CodedValue) -> Unit, onBufferSizeSelected: (Int) -> Unit, onApplyButtonClicked: () -> Unit,) { Column( modifier = Modifier .padding(24.dp) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( text = stringResource(R.string.add_feature), style = MaterialTheme.typography.titleLarge, modifier = Modifier.align(Alignment.CenterHorizontally) )
Spacer(Modifier.size(8.dp)) 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.selectedStatusAttribute, availableValues = featureEditState.statusAttributes, onNewValueSelected = onStatusAttributeSelected ) AttributeDropdown( attributeName = stringResource(R.string.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.selectedBufferSize) } val bufferRange by remember (key1 = featureEditState ) { mutableStateOf(featureEditState.bufferRange) }
// 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( 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(), steps = (bufferRange.last - bufferRange.first), onValueChange = { bufferSize = it.roundToInt() }, onValueChangeFinished = { onBufferSizeSelected(bufferSize) }, track = { sliderState -> SliderDefaults.Track( enabled = bufferRange.first != bufferRange.last, sliderState = sliderState, drawStopIndicator = null, drawTick = { _, _ -> } ) } )
HorizontalDivider() Text( text = stringResource(R.string.contingent_note), style = MaterialTheme.typography.labelLarge ) Button( modifier = Modifier.align(Alignment.CenterHorizontally), onClick = onApplyButtonClicked ) { Text(stringResource(R.string.apply)) }
}}
@OptIn(ExperimentalMaterial3Api::class)@Composablefun AttributeDropdown( attributeName: String, codedValue: CodedValue?, availableValues: List<CodedValue>, onNewValueSelected: (CodedValue) -> Unit) { var expanded by rememberSaveable { mutableStateOf(false) } ExposedDropdownMenuBox( modifier = Modifier.fillMaxWidth(), onExpandedChange = { if (availableValues.isNotEmpty()) { expanded = !expanded } }, expanded = expanded ) {
val textValue = codedValue?.name
OutlinedTextField( enabled = availableValues.isNotEmpty(), modifier = Modifier .fillMaxWidth() .menuAnchor(type = ExposedDropdownMenuAnchorType.PrimaryNotEditable), value = textValue ?: "", onValueChange = {}, label = { Text(attributeName) }, readOnly = true )
ExposedDropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } ) { availableValues.forEach { value -> DropdownMenuItem( text = { Text(value.name) }, onClick = { expanded = false onNewValueSelected(value) } ) } } }}
@Preview(showBackground = true)@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)@Composablefun SheetPreview() { SampleAppTheme { Surface { BottomSheetContents( featureEditState = FeatureEditState(), onStatusAttributeSelected = { }, onProtectionAttributeSelected = { }, onBufferSizeSelected = { }, onApplyButtonClicked = { }, ) } }}