Snap geometry edits with utility network rules

View on GitHubSample viewer app

Use the Geometry Editor to edit geometries using utility network connectivity rules.

Screenshot of snap geometry edits with utility network rules

Use case

A field worker can create new features in a utility network by editing and snapping the vertices of a geometry to existing features on a map. In a gas utility network, gas pipeline features can be represented with the polyline geometry type. Utility networks use geometric coincident-based connectivity to provide pathways for resources. Rule-based snapping uses utility network connectivity rules when editing features based on their asset type and asset group to help maintain network connectivity.

How to use the sample

To edit a geometry, tap a point geometry to be edited in the map to select it. Then edit the geometry by tapping the button to start the geometry edito with the reticle tool.

Snap sources can be enabled and disabled. Snapping will not occur when SnapRuleBehavior.RulesPreventSnapping even when the source is enabled.

To interactively snap a vertex to a feature or graphic, ensure that snapping is enabled for the relevant snap source, tap the reticle to pick up the point geometry, then move the map to position the reticle nearby an existing feature or graphic. If the existing feature or graphic has valid utility network connectivity rules for the asset type that is being created or edited, the edit position will be adjusted to coincide with (or snap to) edges and vertices of its geometry. Tap again to place the vertex at the snapped location.

To discard changes and stop the geometry editor, press the discard button.

To save your edits, press the save button.

How it works

  1. Create a map with LoadSettings.FeatureTilingMode set to EnabledWithFullResolutionWhenSupported.

  2. Create a Geodatabase using the mobile geodatabase file location.

  3. Display Geodatabase.featureTables on the map using subtype feature layers.

  4. Create a GeometryEditor, create a ReticleVertexTool and set this onto GeometryEditor.tool, and connect the editor to the map view.

  5. When editing a feature:

    a. Call SnapRules.create(UtilityNetwork, UtilityAssetType) to get the snap rules associated with a given UtilityAssetType.

    b. Use syncSourceSettings(SnapRules, SnapSourceEnablingBehavior.setFromRules) to populate the snapSettings.sourceSettings with SnapSourceSettings enabling the sources with rules.

  6. Start the geometry editor with an existing geometry or GeometryType.Point.

Relevant API

  • FeatureLayer
  • Geometry
  • GeometryEditor
  • GeometryEditorStyle
  • GraphicsOverlay
  • MapView
  • ReticleVertexTool
  • SnapRuleBehavior
  • SnapRules
  • SnapSettings
  • SnapSource
  • SnapSourceEnablingBehavior
  • SnapSourceSettings
  • UtilityNetwork

About the data

The Naperville gas network mobile geodatabase contains a utility network with a set of connectivity rules that can be used to perform geometry edits with rules based snapping.

Tags

edit, feature, geometry editor, graphics, layers, map, reticle, snapping, utility network

Sample Code

SnapGeometryEditsWithUtilityNetworkRulesViewModel.ktSnapGeometryEditsWithUtilityNetworkRulesViewModel.ktMainActivity.ktDownloadActivity.ktSnapGeometryEditsWithUtilityNetworkRulesScreen.ktSnapSourcesPanel.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
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
/* Copyright 2025 Esri
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.esri.arcgismaps.sample.snapgeometryeditswithutilitynetworkrules.components

import android.app.Application
import android.graphics.drawable.BitmapDrawable
import androidx.compose.ui.unit.dp
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.Geodatabase
import com.arcgismaps.data.GeodatabaseFeatureTable
import com.arcgismaps.geometry.Geometry
import com.arcgismaps.geometry.GeometryType
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.mapping.layers.FeatureLayer
import com.arcgismaps.mapping.layers.FeatureTilingMode
import com.arcgismaps.mapping.layers.SubtypeFeatureLayer
import com.arcgismaps.mapping.layers.SubtypeSublayer
import com.arcgismaps.mapping.symbology.Renderer
import com.arcgismaps.mapping.symbology.SimpleLineSymbol
import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle
import com.arcgismaps.mapping.symbology.SimpleRenderer
import com.arcgismaps.mapping.symbology.Symbol
import com.arcgismaps.mapping.view.Graphic
import com.arcgismaps.mapping.view.GraphicsOverlay
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
import com.arcgismaps.mapping.view.geometryeditor.ReticleVertexTool
import com.arcgismaps.mapping.view.geometryeditor.SnapRuleBehavior
import com.arcgismaps.mapping.view.geometryeditor.SnapRules
import com.arcgismaps.mapping.view.geometryeditor.SnapSourceEnablingBehavior
import com.arcgismaps.mapping.view.geometryeditor.SnapSourceSettings
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
import com.arcgismaps.utilitynetworks.UtilityAssetType
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
import com.esri.arcgismaps.sample.snapgeometryeditswithutilitynetworkrules.R
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.io.File

class SnapGeometryEditsWithUtilityNetworkRulesViewModel(application: Application) : AndroidViewModel(application) {

    // Define the map view proxy and map with basemap and initial extent
    val mapViewProxy = MapViewProxy()

    val arcGISMap = ArcGISMap(basemapStyle = BasemapStyle.ArcGISStreetsNight).apply {
        // Geodatabase layers are always full extent, however if using feature service layers, we
        // must ensure that tiles use full resolution in order to snap to features
        loadSettings.featureTilingMode = FeatureTilingMode.EnabledWithFullResolutionWhenSupported
        initialViewpoint = Viewpoint(
            center = Point(-9811055.156028448, 5131792.19502501, SpatialReference.webMercator()),
            scale = 1e4
        )
    }

    // Get the file path of the geodatabase file
    private val provisionPath: String by lazy {
        application.getExternalFilesDir(null)?.path.toString() + File.separator +
                application.getString(R.string.snap_geometry_edits_with_utility_network_rules_app_name)
    }
    private val filePath = provisionPath + application.getString(R.string.naperville_geodatabase)

    // Create the mobile map package
    private val geodatabase = Geodatabase(filePath)

    // Hold references to the subtype sublayers for the distribution and service pipe layers
    private var distributionPipeLayer: SubtypeSublayer? = null
    private var servicePipeLayer: SubtypeSublayer? = null
    private val pipelineLayerName = application.getString(R.string.pipeline_layer_name)
    private val distributionPipeName = application.getString(R.string.distribution_pipe_name)
    private val servicePipeLayerName = application.getString(R.string.service_pipe_layer_name)

    // Message dialog view model for handling error messages
    val messageDialogVM = MessageDialogViewModel()

    // Symbols to help visualize the snap rules behaviors
    private val rulesPreventSymbol: Symbol =
        SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.red, 4f)
    private val rulesLimitSymbol: Symbol =
        SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.fromRgba(255, 165, 0 ), 3f)
    private val noneSymbol: Symbol =
        SimpleLineSymbol(SimpleLineSymbolStyle.Dash, Color.green, 3f)
    private val symbols = mutableMapOf(
        SnapRuleBehavior.RulesPreventSnapping to rulesPreventSymbol,
        SnapRuleBehavior.RulesLimitSnapping to rulesLimitSymbol,
        SnapRuleBehavior.None to noneSymbol
    )
    private val symbolSwatches = mutableMapOf<SnapRuleBehavior, BitmapDrawable?>()

    // Save default renderers to reset the layers when a feature selection is cleared
    private var defaultDistributionRenderer: Renderer? = null
    private var defaultServiceRenderer: Renderer? = null

    // Define a geometry editor with a snapping enabled. A reticle tool is ideal for touch devices.
    val geometryEditor = GeometryEditor().apply {
        snapSettings.isEnabled = true
        snapSettings.isFeatureSnappingEnabled = true
        tool = ReticleVertexTool()
    }

    // Define a graphics overlay which can also act as a snap source
    private val defaultGraphicRenderer = SimpleRenderer(SimpleLineSymbol(
        SimpleLineSymbolStyle.Dash, Color.fromRgba(165, 165, 165), 3f))
    val graphicsOverlay = GraphicsOverlay(
        graphics = listOf(Graphic(Geometry.fromJsonOrNull(application.getString(R.string.graphic_geometry_json))))
    ).apply {
        id = application.getString(R.string.graphics_overlay_id)
        renderer = defaultGraphicRenderer
    }

    // Hold a reference to a selected feature and its related asset group and type
    private var selectedFeature: ArcGISFeature? = null

    private val _assetGroupNameState = MutableStateFlow("<Nothing selected>")
    val assetGroupNameState = _assetGroupNameState.asStateFlow()

    private val _assetTypeNameState = MutableStateFlow("<Nothing selected>")
    val assetTypeNameState = _assetTypeNameState.asStateFlow()

    // Represents the snap source settings object (for enabling and disabling), along with a name
    // to use in the UI, and a symbol swatch
    data class SnapSourceProperty(
        val name: String,
        val swatch: BitmapDrawable,
        val snapSourceSettings: SnapSourceSettings
    )
    private val _snapSourcePropertyList = MutableStateFlow(listOf<SnapSourceProperty>())
    val snapSourcePropertyList = _snapSourcePropertyList.asStateFlow()

    // Create boolean flags to track the state of UI components
    private val _isEditButtonEnabled = MutableStateFlow(false)
    internal val isEditButtonEnabled = _isEditButtonEnabled.asStateFlow()

    init {
        viewModelScope.launch {
            arcGISMap.load().onFailure { error ->
                messageDialogVM.showMessageDialog(error)
            }

            // Load the mobile map package
            geodatabase.load().onSuccess {
                // Add layers from the geodatabase to the map
                addLayersToMapFromGeodatabase(application)

                // Set the utility network on the map and load it
                arcGISMap.utilityNetworks.add(geodatabase.utilityNetworks.first())
                arcGISMap.utilityNetworks.first().load().onFailure {
                    messageDialogVM.showMessageDialog(it)
                }

                // Set up symbol swatches
                symbolSwatches[SnapRuleBehavior.RulesPreventSnapping] = createSwatch(rulesPreventSymbol)
                symbolSwatches[SnapRuleBehavior.RulesLimitSnapping] = createSwatch(rulesLimitSymbol)
                symbolSwatches[SnapRuleBehavior.None] = createSwatch(noneSymbol)
            }.onFailure {
                messageDialogVM.showMessageDialog(it)
            }
        }
    }

    /**
     * Start the geometry editor to edit the geometry of the selected feature.
     */
    private fun editFeatureGeometry() {
        // Get the symbol for the selected feature
        selectedFeature?.let { feature ->

            // Get the geodatabase feature table of the selected feature
            val featureTable = (feature.featureTable as? GeodatabaseFeatureTable) ?: return
            // Use the symbol from the selected feature in the style of the geometry editor tool
            val symbol = featureTable.layerInfo?.drawingInfo?.renderer?.getSymbol(feature)
            geometryEditor.tool.style.apply {
                vertexSymbol = symbol
                feedbackVertexSymbol = symbol
                selectedVertexSymbol = symbol
                vertexTextSymbol = null
            }

            // Hide the selected feature
            (featureTable.layer as? FeatureLayer)?.setFeatureVisible(feature, false)

            // Start the geometry editor and center the map underneath the reticle
            feature.geometry?.let { initialGeometry ->
                viewModelScope.launch {
                    mapViewProxy.setViewpointCenter(initialGeometry.extent.center)
                }
                geometryEditor.start(initialGeometry)
                geometryEditor.selectVertex(0,0)
            }
        }
    }

    /**
     * Stop the geometry editor and discard the changes made to the geometry.
     */
    fun discardGeometryChanges() {
        // Discard the current edit
        geometryEditor.stop()

        // Reset the selection
        resetSelections()
    }

    /**
     * Stop the geometry editor, and update the previously identified feature with the new geometry.
     */
    fun saveGeometryChanges() {
        // Stop the geometry editor and get the updated geometry
        val finalGeometry = geometryEditor.stop()

        // Update the feature with the new geometry
        selectedFeature?.let { feature ->
            feature.geometry = finalGeometry

            viewModelScope.launch {
                (feature.featureTable as? GeodatabaseFeatureTable)?.updateFeature(feature)
                    ?.onFailure { error -> messageDialogVM.showMessageDialog(error) }
            }
        }

        // Reset the selection
        resetSelections()
    }

    /**
     * Identifies the tapped screen coordinate in the provided [singleTapConfirmedEvent] and gets
     * the asset at that location.
     */
    fun identify(singleTapConfirmedEvent: SingleTapConfirmedEvent) {

        if (geometryEditor.isStarted.value || arcGISMap.operationalLayers.isEmpty()) {
            return
        }
        viewModelScope.launch {
            mapViewProxy.identifyLayers(
                screenCoordinate = singleTapConfirmedEvent.screenCoordinate,
                tolerance = 12.dp,
                returnPopupsOnly = false,
                maximumResults = 1
            ).onSuccess { identifyResultList ->
                // As we are using subtype feature layers in this sample the returned features are
                // contained in the sublayer results
                val identifiedFeature = identifyResultList.firstOrNull()?.sublayerResults?.firstOrNull()?.geoElements?.firstOrNull()
                if (identifiedFeature !is ArcGISFeature) {
                    return@launch resetSelections()
                }

                // In this sample we only allow selection of point features. If the identified
                // feature is null or the feature is not a point feature then reset and return.
                if (identifiedFeature.featureTable?.geometryType != GeometryType.Point) {
                    return@launch resetSelections()
                } else if (
                    selectedFeature != null
                    && identifiedFeature != selectedFeature
                    && selectedFeature?.featureTable?.layer is FeatureLayer
                ) {
                    // If a feature is already selected and the tapped feature is not the selected
                    // feature then clear the previous selection
                    (selectedFeature?.featureTable?.layer as? FeatureLayer)?.clearSelection()
                }

                // Update the selected feature and select it on the layer
                selectedFeature = identifiedFeature
                selectedFeature?.let { feature ->
                    (feature.featureTable?.layer as? FeatureLayer)?.selectFeature(feature)

                    // Create a utility element for the selected feature using the utility network
                    arcGISMap.utilityNetworks.first().createElementOrNull(feature)?.let { element ->
                        // Update values for UI based on the selected feature
                        _assetGroupNameState.value = element.assetGroup.name
                        _assetTypeNameState.value = element.assetType.name
                        _isEditButtonEnabled.value = true
                        setSnapSettings(element.assetType)
                    } ?: return@launch messageDialogVM.showMessageDialog("Error creating UtilityElement")

                    // Start the editing session once feature is selected.
                    editFeatureGeometry()
                }
            }
        }
    }

    /**
     * Creates [SnapRules] based on the given asset type, synchronizes the snap sources using these
     * rules, then updates the snap sources list used by the UI.
     */
    private suspend fun setSnapSettings(assetType: UtilityAssetType) {
        // Get the snap rules associated with the asset type
        val snapRules = SnapRules.create(arcGISMap.utilityNetworks.first(), assetType).getOrElse {
            return messageDialogVM.showMessageDialog(it)
        }

        geometryEditor.snapSettings.apply {
            // Synchronize the snap source collection with the map's operational layers using the snap
            // rules. Setting SnapSourceEnablingBehavior.SetFromRules will enable snapping for the
            // layers and sublayers specified in the snap rules.
            syncSourceSettings(snapRules, SnapSourceEnablingBehavior.SetFromRules)

            // Enable snapping for the graphics overlay as this will not be affected by the given
            // SnapSourceEnablingBehavior.setFromRules
            sourceSettings.first { it.source == graphicsOverlay }.isEnabled = true
        }

        updateSnapSourceList()
    }


    /**
     * Updates the enabled value of the [SnapSourceSettings] object at the given index and rebuilds
     * the snap source list.
     */
    fun setSnapSourceCheckedValue(checkedValue: Boolean, index: Int) {
        // Set new value into appropriate property via index
        _snapSourcePropertyList.value[index].snapSourceSettings.isEnabled = checkedValue

        updateSnapSourceList()
    }

    /**
     * Updates the lists used by the UI to show SnapSourceSettings information.
    */
    private fun updateSnapSourceList() {
        // Update the backing list with a new list of current properties from snapSettings
        _snapSourcePropertyList.value = currentSnapSourcePropertyList()
    }

    /**
     * Returns a list of [SnapSourceProperty] objects based on the current snap sources.
     */
    private fun currentSnapSourcePropertyList(): List<SnapSourceProperty> {
        return buildList {
            geometryEditor.snapSettings.sourceSettings.forEach { sourceSettings ->
                when (sourceSettings.source) {
                    is GraphicsOverlay -> {
                        symbolSwatches[sourceSettings.ruleBehavior]?.let { swatch ->
                            add(SnapSourceProperty((sourceSettings.source as GraphicsOverlay).id, swatch, sourceSettings))
                        }

                        // Set the appropriate symbol for the layer based on the SnapRuleBehavior.
                        graphicsOverlay.renderer = SimpleRenderer(symbols[sourceSettings.ruleBehavior])
                    }
                    is SubtypeFeatureLayer -> {
                        sourceSettings.childSourceSettings.forEach { childSourceSettings ->
                            (childSourceSettings.source as? SubtypeSublayer)?.let { childSource ->
                                when (childSource.name) {
                                    distributionPipeName, servicePipeLayerName -> {
                                        symbolSwatches[childSourceSettings.ruleBehavior]?.let { swatch ->
                                            add(SnapSourceProperty(childSource.name, swatch, childSourceSettings))
                                        }

                                        // Set the appropriate symbol for the sublayer based on the SnapRuleBehavior.
                                        childSource.renderer =
                                            SimpleRenderer(symbols[childSourceSettings.ruleBehavior])
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Clears the selection on the layer and reinstates feature visibility, then resets the selected
     * feature and layer and any UI backing variables.
     */
    private fun resetSelections() {
        // Clear the existing selection and show the selected feature
        selectedFeature?.let { feature ->
            (feature.featureTable?.layer as? FeatureLayer)?.let {
                it.clearSelection()
                it.setFeatureVisible(feature = feature, visible = true)
            }
        }

        // Reset the selected feature and layer
        selectedFeature = null
        _assetGroupNameState.value = "<Nothing selected>"
        _assetTypeNameState.value = "<Nothing selected>"
        _isEditButtonEnabled.value = false

        // Revert back to the default renderer for the distribution and service pipe layers and
        // graphics overlay
        distributionPipeLayer?.renderer = defaultDistributionRenderer
        servicePipeLayer?.renderer = defaultServiceRenderer
        graphicsOverlay.renderer = defaultGraphicRenderer

        // Clear the snap sources list
        _snapSourcePropertyList.value = emptyList()
    }

    /**
     * Adds required layers from the geodatabase to the map, setting the visibility of sublayers to
     * show only a small subset in order to avoid too much visual clutter.
     */
    private suspend fun addLayersToMapFromGeodatabase(application: Application) {
        // Only show the Distribution Pipe and Service Pipe sublayers in the pipeline layer. Also
        // store the default renderer for the these sublayers.
        val pipeLayer = SubtypeFeatureLayer(
            geodatabase.getFeatureTable(pipelineLayerName) as ArcGISFeatureTable
        )
        pipeLayer.load().getOrElse {
            messageDialogVM.showMessageDialog(
                "Error loading pipeline layer",
                it.message.toString()
            )
        }
        val distributionPipeName = application.getString(R.string.distribution_pipe_name)
        val servicePipeLayerName = application.getString(R.string.service_pipe_layer_name)
        //  Set the visibility of the sublayers and store the default renderer for the distribution
        //  and service pipe layers.
        pipeLayer.subtypeSublayers.forEach { sublayer ->
            when (sublayer.name) {
                distributionPipeName -> {
                    distributionPipeLayer = sublayer
                    defaultDistributionRenderer = sublayer.renderer
                }
                servicePipeLayerName -> {
                    servicePipeLayer = sublayer
                    defaultServiceRenderer = sublayer.renderer
                }
                else -> {
                    // Hide all other sublayers
                    sublayer.isVisible = false
                }
            }
        }
        arcGISMap.operationalLayers.add(pipeLayer)

        // Only show the Excess Flow Valve and Controllable Tee sublayers in the device layer
        val deviceLayer = SubtypeFeatureLayer(
            geodatabase.getFeatureTable(
                application.getString(R.string.device_layer_name)
            ) as ArcGISFeatureTable
        )
        deviceLayer.load().getOrElse {
            messageDialogVM.showMessageDialog(
                "Error loading pipeline layer",
                it.message.toString()
            )
        }
        val excessFlowValveName = application.getString(R.string.excess_flow_valve_name)
        val controllableTeeName = application.getString(R.string.controllable_tee_name)
        // Hide all sublayers that aren't excess flow valves or controllable tees
        deviceLayer.subtypeSublayers.filter { sublayer ->
            sublayer.name != excessFlowValveName && sublayer.name != controllableTeeName
        }.forEach { sublayerToHide ->
            sublayerToHide.isVisible = false
        }
        // Add the device layer to the map
        arcGISMap.operationalLayers.add(deviceLayer)

        val junctionLayer = (geodatabase.getFeatureTable(
            tableName = application.getString(R.string.junction_layer_name)
        ) as? ArcGISFeatureTable)?.let { junctionFeatureTable ->
            SubtypeFeatureLayer(featureTable = junctionFeatureTable)
        } ?: return messageDialogVM.showMessageDialog("Error retrieving junction layer")

        // Add the junction layer to the map
        arcGISMap.operationalLayers.add(junctionLayer)
    }

    /**
     * Create a swatch from the given symbol.
     */
    private suspend fun createSwatch(symbol: Symbol): BitmapDrawable? {
        // Create a swatch from the symbol
        val swatch = symbol.createSwatch(
            screenScale = 30.0f,
            width = 4.0f,
            height = 4.0f
        ).getOrNull()

        return swatch
    }
}

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