Query features with arcade expression

View on GitHubSample viewer app

Query features on a map using an Arcade expression.

QueryFeaturesWithArcadeExpression

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

  1. Create a PortalItem using the URL and ID.

  2. Create an ArcGISMap using the portal item.

  3. Create a MapViewProxy to handle user interaction with the map view.

  4. Provide behaviour for the MapView's onSingleTapConfirmed parameter to react to taps on the map.

  5. Identify the visible layer where it is tapped using mapViewProxy.identify() and get the feature from the result.

  6. Create the following ArcadeExpression:

    expressionValue = "var crimes = FeatureSetByName(\$map, 'Crime in the last 60 days');\n" +
     "return Count(Intersects(\$feature, crimes));"
  7. Create an ArcadeEvaluator using the Arcade expression and ArcadeProfile.FormCalculation.

  8. Create a map of profile variables with the following key-value pairs:

     mapOf<String, Any>("\$feature" to feature, "\$map" to mapView.map)
  9. Call ArcadeEvaluator.evaluate() on the Arcade evaluator object and pass the profile variables map.

  10. Get the ArcadeEvaluationResult.result.

  11. 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

QueryFeaturesWithArcadeExpressionViewModel.ktQueryFeaturesWithArcadeExpressionViewModel.ktMainActivity.ktQueryFeaturesWithArcadeExpressionScreen.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
/* 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.Application
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.arcgismaps.arcade.ArcadeEvaluator
import com.arcgismaps.arcade.ArcadeExpression
import com.arcgismaps.arcade.ArcadeProfile
import com.arcgismaps.data.ArcGISFeature
import com.arcgismaps.geometry.Point
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.PortalItem
import com.arcgismaps.mapping.layers.Layer
import com.arcgismaps.mapping.symbology.PictureMarkerSymbol
import com.arcgismaps.mapping.view.Graphic
import com.arcgismaps.mapping.view.GraphicsOverlay
import com.arcgismaps.mapping.view.ScreenCoordinate
import com.arcgismaps.portal.Portal
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
import com.esri.arcgismaps.sample.queryfeatureswitharcadeexpression.R
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import 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
}

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