Use the Geometry Editor to edit geometries using utility network connectivity 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
-
Create a map with
LoadSettings.FeatureTilingModeset toEnabledWithFullResolutionWhenSupported. -
Create a
Geodatabaseusing the mobile geodatabase file location. -
Display
Geodatabase.featureTableson the map using subtype feature layers. -
Create a
GeometryEditor, create aReticleVertexTooland set this ontoGeometryEditor.tool, and connect the editor to the map view. -
When editing a feature:
a. Call
SnapRules.create(UtilityNetwork, UtilityAssetType)to get the snap rules associated with a givenUtilityAssetType.b. Use
syncSourceSettings(SnapRules, SnapSourceEnablingBehavior.setFromRules)to populate thesnapSettings.sourceSettingswithSnapSourceSettingsenabling the sources with rules. -
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
/* 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
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.snap_geometry_edits_with_utility_network_rules_app_name), listOf( // ArcGIS Portal item containing the mobile geodatabase containing data for the // Napperville gas utility network "https://www.arcgis.com/home/item.html?id=0fd3a39660d54c12b05d5f81f207dffd", ) ) }}/* 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
import android.annotation.SuppressLintimport android.content.pm.ActivityInfoimport 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.snapgeometryeditswithutilitynetworkrules.screens.SnapGeometryEditsWithUtilityNetworkRulesScreen
class MainActivity : ComponentActivity() {
@SuppressLint("SourceLockedOrientationActivity") 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) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
enableEdgeToEdge() setContent { SampleAppTheme { SnapGeometryEditsWithUtilityNetworkRulesApp() } } }
@Composable private fun SnapGeometryEditsWithUtilityNetworkRulesApp() { Surface(color = MaterialTheme.colorScheme.background) { SnapGeometryEditsWithUtilityNetworkRulesScreen( sampleName = getString(R.string.snap_geometry_edits_with_utility_network_rules_app_name) ) } }}/* 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.screens
import android.content.res.Configurationimport androidx.compose.animation.animateContentSizeimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Surfaceimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.text.style.TextAlignimport androidx.compose.ui.tooling.preview.Previewimport androidx.compose.ui.unit.dpimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.sampleslib.components.BottomSheetimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.snapgeometryeditswithutilitynetworkrules.components.SnapGeometryEditsWithUtilityNetworkRulesViewModel
/** * Main screen layout for the sample app */@Composablefun SnapGeometryEditsWithUtilityNetworkRulesScreen(sampleName: String) { val mapViewModel = viewModel<SnapGeometryEditsWithUtilityNetworkRulesViewModel>()
// Collect latest UI state changes val isEditButtonEnabled by mapViewModel.isEditButtonEnabled.collectAsStateWithLifecycle() val assetGroupNameState by mapViewModel.assetGroupNameState.collectAsStateWithLifecycle() val assetTypeNameState by mapViewModel.assetTypeNameState.collectAsStateWithLifecycle() val snapSourcePropertyList by mapViewModel.snapSourcePropertyList.collectAsStateWithLifecycle() val isGeometryEditorStarted by mapViewModel.geometryEditor.isStarted.collectAsStateWithLifecycle() val canGeometryEditorUndo by mapViewModel.geometryEditor.canUndo.collectAsStateWithLifecycle()
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { Column( modifier = Modifier .fillMaxSize() .padding(it), ) { Column(modifier = Modifier.animateContentSize()) { if (snapSourcePropertyList.isEmpty()) { Text( modifier = Modifier .fillMaxWidth() .padding(8.dp), text = "Tap a point feature to edit", textAlign = TextAlign.Center, style = MaterialTheme.typography.labelMedium ) } else { if (isEditButtonEnabled) { TopHeader(assetGroupNameState, assetTypeNameState) } } } MapView( modifier = Modifier .fillMaxSize() .weight(1f), arcGISMap = mapViewModel.arcGISMap, geometryEditor = mapViewModel.geometryEditor, graphicsOverlays = listOf(mapViewModel.graphicsOverlay), mapViewProxy = mapViewModel.mapViewProxy, onSingleTapConfirmed = mapViewModel::identify ) }
BottomSheet(isVisible = snapSourcePropertyList.isNotEmpty()) { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .animateContentSize() ) { SnapSourcesPanel( snapSourcePropertyList = snapSourcePropertyList, onSnapSourcePropertyChanged = mapViewModel::setSnapSourceCheckedValue, isGeometryEditorStarted = isGeometryEditorStarted, canGeometryEditorUndo = canGeometryEditorUndo, onDiscardGeometryChanges = mapViewModel::discardGeometryChanges, onSaveGeometryChanges = mapViewModel::saveGeometryChanges, ) } }
mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}
@Composablefun TopHeader( assetGroupNameState: String, assetTypeNameState: String) { Column( modifier = Modifier .fillMaxWidth() .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( text = "Feature selected:", style = MaterialTheme.typography.titleMedium ) Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = "AssetGroup: ", style = MaterialTheme.typography.labelMedium, ) Text( text = assetGroupNameState, style = MaterialTheme.typography.labelSmall ) } Row(verticalAlignment = Alignment.CenterVertically) { Text( text = "AssetType: ", style = MaterialTheme.typography.labelMedium ) Text( text = assetTypeNameState, style = MaterialTheme.typography.labelSmall ) } } }}
@Preview(showBackground = true)@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)@Composablefun TopHeaderPreview() { SampleAppTheme { Surface { TopHeader( assetGroupNameState = "TestGroupName", assetTypeNameState = "TestTypeName" ) } }}/* 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.screens
import android.content.res.Configurationimport androidx.compose.foundation.Imageimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Checkimport androidx.compose.material.icons.filled.Deleteimport androidx.compose.material3.Iconimport androidx.compose.material3.IconButtonimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.material3.Switchimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.asImageBitmapimport androidx.compose.ui.tooling.preview.Previewimport androidx.compose.ui.unit.dpimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.snapgeometryeditswithutilitynetworkrules.components.SnapGeometryEditsWithUtilityNetworkRulesViewModel
@Composablefun SnapSourcesPanel( snapSourcePropertyList: List<SnapGeometryEditsWithUtilityNetworkRulesViewModel.SnapSourceProperty>, onSnapSourcePropertyChanged: (Boolean, Int) -> Unit, isGeometryEditorStarted: Boolean, canGeometryEditorUndo: Boolean, onDiscardGeometryChanges: () -> Unit, onSaveGeometryChanges: () -> Unit) { Column( modifier = Modifier .fillMaxWidth() .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp) ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { IconButton( enabled = isGeometryEditorStarted, onClick = onDiscardGeometryChanges ) { Icon(imageVector = Icons.Default.Delete, contentDescription = "Discard") } Text( text = "Snap source settings", style = MaterialTheme.typography.titleMedium )
IconButton( enabled = (isGeometryEditorStarted && canGeometryEditorUndo), onClick = onSaveGeometryChanges ) { Icon(imageVector = Icons.Default.Check, contentDescription = "Save") } }
snapSourcePropertyList.forEachIndexed { index, snapSourceProperty -> Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Image( bitmap = snapSourceProperty.swatch.bitmap.asImageBitmap(), contentDescription = "Symbol image" ) Switch( checked = snapSourcePropertyList[index].snapSourceSettings.isEnabled, onCheckedChange = { newValue -> onSnapSourcePropertyChanged(newValue, index) } ) Text( style = MaterialTheme.typography.bodySmall, text = "${snapSourceProperty.name} (${snapSourceProperty.snapSourceSettings.ruleBehavior})", ) } } }}
@Preview(showBackground = true)@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)@Composablefun SnapSourcesPanelPreview() { SampleAppTheme { Surface { SnapSourcesPanel( snapSourcePropertyList = listOf(), onSnapSourcePropertyChanged = { _, _ -> }, isGeometryEditorStarted = true, canGeometryEditorUndo = true, onDiscardGeometryChanges = {}, onSaveGeometryChanges = {} ) } }}/* 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.Applicationimport android.graphics.drawable.BitmapDrawableimport androidx.compose.ui.unit.dpimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.Colorimport com.arcgismaps.data.ArcGISFeatureimport com.arcgismaps.data.ArcGISFeatureTableimport com.arcgismaps.data.Geodatabaseimport com.arcgismaps.data.GeodatabaseFeatureTableimport com.arcgismaps.geometry.Geometryimport com.arcgismaps.geometry.GeometryTypeimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.layers.FeatureTilingModeimport com.arcgismaps.mapping.layers.SubtypeFeatureLayerimport com.arcgismaps.mapping.layers.SubtypeSublayerimport com.arcgismaps.mapping.symbology.Rendererimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleRendererimport com.arcgismaps.mapping.symbology.Symbolimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.mapping.view.geometryeditor.GeometryEditorimport com.arcgismaps.mapping.view.geometryeditor.ReticleVertexToolimport com.arcgismaps.mapping.view.geometryeditor.SnapRuleBehaviorimport com.arcgismaps.mapping.view.geometryeditor.SnapRulesimport com.arcgismaps.mapping.view.geometryeditor.SnapSourceEnablingBehaviorimport com.arcgismaps.mapping.view.geometryeditor.SnapSourceSettingsimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxyimport com.arcgismaps.utilitynetworks.UtilityAssetTypeimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport com.esri.arcgismaps.sample.snapgeometryeditswithutilitynetworkrules.Rimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.launchimport 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 }}