Query features on a map using an Arcade expression.

Use case
Arcade is a portable, lightweight, and secure expression language used to create custom content in ArcGIS applications. Like other expression languages, it can perform mathematical calculations, manipulate text, and evaluate logical statements. It also supports multi-statement expressions, variables, and flow control statements. What makes Arcade particularly unique when compared to other expression and scripting languages is its inclusion of feature and geometry data types. This sample uses an Arcade expression to query the number of crimes in a neighborhood in the last 60 days.
How to use the sample
Tap on any neighborhood to see the number of crimes in the last 60 days in a TextView.
How it works
-
Create a
PortalItemusing the URL and ID. -
Create an
ArcGISMapusing the portal item. -
Create a
MapViewProxyto handle user interaction with the map view. -
Provide behaviour for the
MapView’sonSingleTapConfirmedparameter to react to taps on the map. -
Identify the visible layer where it is tapped using
mapViewProxy.identify()and get the feature from the result. -
Create the following
ArcadeExpression:expressionValue = "var crimes = FeatureSetByName(\$map, 'Crime in the last 60 days');\n" +"return Count(Intersects(\$feature, crimes));" -
Create an
ArcadeEvaluatorusing the Arcade expression andArcadeProfile.FormCalculation. -
Create a map of profile variables with the following key-value pairs:
mapOf<String, Any>("\$feature" to feature, "\$map" to mapView.map) -
Call
ArcadeEvaluator.evaluate()on the Arcade evaluator object and pass the profile variables map. -
Get the
ArcadeEvaluationResult.result. -
Convert the result to a numerical value (
Double) and pass it to the UI.
Relevant API
- ArcadeEvaluationResult
- ArcadeEvaluator
- ArcadeExpression
- ArcadeProfile
- Portal
- PortalItem
About the data
This sample uses the Crimes in Police Beats Sample ArcGIS Online Web Map which contains 2 layers for city beats borders, and crimes in the last 60 days as recorded by the Rochester, NY police department.
Additional information
This sample uses the GeoView-Compose module of the ArcGIS Maps SDK for Kotlin Toolkit to implement a Composable MapView.
Visit Getting Started on the ArcGIS Developer website to learn more about Arcade expressions.
Tags
Arcade evaluator, Arcade expression, geoview-compose, identify layers, portal, portal item, query, 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.queryfeatureswitharcadeexpression
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.queryfeatureswitharcadeexpression.screens.QueryFeaturesWithArcadeExpressionScreen
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 { QueryFeaturesWithArcadeExpressionApp() } } }
@Composable private fun QueryFeaturesWithArcadeExpressionApp() { Surface(color = MaterialTheme.colorScheme.background) { QueryFeaturesWithArcadeExpressionScreen( sampleName = getString(R.string.query_features_with_arcade_expression_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.queryfeatureswitharcadeexpression.components
import android.app.Applicationimport android.graphics.BitmapFactoryimport android.graphics.drawable.BitmapDrawableimport androidx.compose.ui.unit.dpimport androidx.core.graphics.drawable.toDrawableimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.arcade.ArcadeEvaluatorimport com.arcgismaps.arcade.ArcadeExpressionimport com.arcgismaps.arcade.ArcadeProfileimport com.arcgismaps.data.ArcGISFeatureimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.layers.Layerimport com.arcgismaps.mapping.symbology.PictureMarkerSymbolimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxyimport com.esri.arcgismaps.sample.queryfeatureswitharcadeexpression.Rimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.launch
class QueryFeaturesWithArcadeExpressionViewModel(application: Application) : AndroidViewModel(application) {
// setup the red pin marker image as a bitmap drawable private val markerDrawable: BitmapDrawable by lazy { val bitmap = BitmapFactory.decodeResource(application.resources, R.drawable.map_pin_symbol) bitmap.toDrawable(application.resources) }
// setup the red pin marker as a Graphic private val markerGraphic: Graphic by lazy { val markerSymbol = PictureMarkerSymbol.createWithImage(markerDrawable).apply { width = 30f height = 30f offsetY = 25f }
Graphic(symbol = markerSymbol) }
// data layer to be loaded from portal item private var policeBeatsLayer: Layer? = null
// state flow to expose query results and status to UI private val _queryStateFlow = MutableStateFlow(QueryState(loadState = LoadState.LOADING)) val queryStateFlow = _queryStateFlow.asStateFlow()
val graphicsOverlay = GraphicsOverlay()
// create a portal item with the itemId of the web map private val portal = Portal("https://www.arcgis.com/") private val portalItem = PortalItem(portal = portal, itemId = "539d93de54c7422f88f69bfac2aebf7d")
// create a map from the portal item val arcGISMap = ArcGISMap(portalItem)
// create a map view proxy for handling interactions with the map view val mapViewProxy = MapViewProxy()
// create a message dialog view model for handling error messages val messageDialogVM = MessageDialogViewModel()
init { viewModelScope.launch { arcGISMap.load().onFailure { error -> messageDialogVM.showMessageDialog( "Failed to load map", error.message.toString() ) }
// get the RPD Beats layer from the map's operational layers policeBeatsLayer = arcGISMap.operationalLayers.firstOrNull { layer -> layer.id == "RPD_Reorg_9254" }
// update query state, map is ready for user interaction _queryStateFlow.value = QueryState(loadState = LoadState.READY_TO_START) }
// add the marker graphic to the graphics overlay graphicsOverlay.graphics.add(markerGraphic) }
/** * Handle a tap on the map view from the user */ fun handleTap(point: Point, screenCoordinate: ScreenCoordinate) { // update the marker location to where the user tapped on the map markerGraphic.geometry = point viewModelScope.launch { // centre the viewpoint on where the user tapped on the map mapViewProxy.setViewpointCenter(point)
// evaluate an Arcade expression on the tapped screen coordinate evaluateArcadeExpression(screenCoordinate) } }
/** * Evaluates an Arcade expression that returns crime in the last 60 days at the tapped * [screenCoordinate] on the [arcGISMap] with the [policeBeatsLayer] and outputs the result * to the [queryStateFlow] property. */ private suspend fun evaluateArcadeExpression(screenCoordinate: ScreenCoordinate) { policeBeatsLayer?.let { layer -> // show the loading spinner as the Arcade evaluation can take time to complete _queryStateFlow.value = QueryState(loadState = LoadState.LOADING)
// do an identify operation on the policeBeatsLayer, using the position tapped on the // mapView, and get the result val result = mapViewProxy.identify( layer = layer, screenCoordinate = screenCoordinate, tolerance = 12.dp, returnPopupsOnly = false )
// get the result as an IdentifyLayerResult val identifyLayerResult = result.getOrElse { error -> // if the identify operation failed show an error and return messageDialogVM.showMessageDialog( "Error performing identify operation:", error.message.toString() ) // reset the query results and loading indicator _queryStateFlow.value = QueryState() return }
if (identifyLayerResult.geoElements.isEmpty()) { _queryStateFlow.value = QueryState(loadState = LoadState.LOADED) return }
// get the first identified GeoElement as an ArcGISFeature val identifiedFeature = identifyLayerResult.geoElements.first() as ArcGISFeature // create a string containing the Arcade expression val expressionValue = "var crimes = FeatureSetByName(\$map, 'Crime in the last 60 days');\n" + "return Count(Intersects(\$feature, crimes));"
// create an arcade expression from the string and configure an arcade evaluator val arcadeExpression = ArcadeExpression(expressionValue) val arcadeEvaluator = ArcadeEvaluator(arcadeExpression, ArcadeProfile.FormCalculation)
// create a map of profile variables with the feature and arcGISMap as key-value pairs val profileVariables = mapOf<String, Any>("\$feature" to identifiedFeature, "\$map" to arcGISMap) // evaluate the arcade expression using these profile variables, and get the result val evaluationResult = arcadeEvaluator.evaluate(profileVariables) val arcadeEvaluationResult = evaluationResult.getOrElse { error -> messageDialogVM.showMessageDialog("Error", error.message.toString()) _queryStateFlow.value = QueryState() return }
_queryStateFlow.value = QueryState(arcadeEvaluationResult.result as Double, LoadState.LOADED) } }}
data class QueryState(val crimes: Double? = null, val loadState: LoadState = LoadState.READY_TO_START)
enum class LoadState { READY_TO_START, LOADING, LOADED}/* 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.queryfeatureswitharcadeexpression.screens
import androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Boximport 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.foundation.layout.widthimport androidx.compose.material3.CircularProgressIndicatorimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.unit.dpimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.queryfeatureswitharcadeexpression.Rimport com.esri.arcgismaps.sample.queryfeatureswitharcadeexpression.components.LoadStateimport com.esri.arcgismaps.sample.queryfeatureswitharcadeexpression.components.QueryFeaturesWithArcadeExpressionViewModelimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
/** * Main screen layout for the sample app */@Composablefun QueryFeaturesWithArcadeExpressionScreen(sampleName: String) { val mapViewModel: QueryFeaturesWithArcadeExpressionViewModel = viewModel()
val queryState by mapViewModel.queryStateFlow.collectAsStateWithLifecycle()
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { Column( modifier = Modifier .fillMaxSize() .padding(it), ) { Box( Modifier .fillMaxSize() .weight(1f), contentAlignment = Alignment.Center ) { MapView( modifier = Modifier.fillMaxSize(), arcGISMap = mapViewModel.arcGISMap, graphicsOverlays = listOf(mapViewModel.graphicsOverlay), mapViewProxy = mapViewModel.mapViewProxy, onSingleTapConfirmed = { tapEvent -> tapEvent.mapPoint?.let { point -> mapViewModel.handleTap( point = point, screenCoordinate = tapEvent.screenCoordinate ) } } ) if (queryState.loadState == LoadState.LOADING) { CircularProgressIndicator( modifier = Modifier.width(96.dp), ) } } Row( Modifier .fillMaxWidth() .padding(10.dp), horizontalArrangement = Arrangement.Center ) { val resultText = queryState.crimes?.let { stringResource(R.string.crime_info_text, it.toInt()) } ?: stringResource(R.string.no_features_found)
Text( text = when (queryState.loadState) { LoadState.READY_TO_START -> stringResource(R.string.tap_to_begin) LoadState.LOADING -> stringResource(R.string.loading) LoadState.LOADED -> resultText }, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurfaceVariant ) } }
mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}