Add features with contingent values

View on GitHubSample viewer app

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

Add features with contingent values

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

  1. Create and load the Geodatabase from the mobile geodatabase location on file.
  2. Load the first GeodatabaseFeatureTable.
  3. Load the ContingentValuesDefinition from the feature table.
  4. Create a new FeatureLayer from the feature table and add it to the map.
  5. Create a new ArcGISFeature using GeodatabaseFeatureTable.createFeature()
  6. Get the first field by name using ArcGISFeatureTable.fields.find{ }.
  7. Then get the Field.domain as an CodedValueDomain.
  8. Get the coded value domain's codedValues to get an array of CodedValue's.
  9. 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 the ContingentValueResults by using ArcGISFeatureTable.getContingentValues(ArcGISFeature, "field_name") with the feature and the target field by name.
    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

  • 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

AddFeaturesWithContingentValuesViewModel.ktAddFeaturesWithContingentValuesViewModel.ktDownloadActivity.ktMainActivity.ktAddFeaturesWithContingentValuesScreen.kt
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
/* 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 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.GeodatabaseFeatureTable
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.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 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
)

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.