Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate "Query features with Arcade expression" to compose #294

Merged
merged 6 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions samples/query-features-with-arcade-expression/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,26 @@ Tap on any neighborhood to see the number of crimes in the last 60 days in a Tex

1. Create a `PortalItem` using the URL and ID.
2. Create an `ArcGISMap` using the portal item.
3. Set up a listener for taps on the map.
4. Identify the visible layer where it is tapped using `mapView.identifyLayer()` and get the feature.
5. Create the following `ArcadeExpression`:
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`:

```kotlin
expressionValue = "var crimes = FeatureSetByName(\$map, 'Crime in the last 60 days');\n" +
"return Count(Intersects(\$feature, crimes));"
```

6. Create an `ArcadeEvaluator` using the Arcade expression and `ArcadeProfile.FormCalculation`.
7. Create a map of profile variables with the following key-value pairs:
7. Create an `ArcadeEvaluator` using the Arcade expression and `ArcadeProfile.FormCalculation`.
8. Create a map of profile variables with the following key-value pairs:

```kotlin
mapOf<String, Any>("\$feature" to feature, "\$map" to mapView.map)
```

8. Call `ArcadeEvaluator.evaluate()` on the Arcade evaluator object and pass the profile variables map.
9. Get the `ArcadeEvaluationResult.result`.
10. Convert the result to a numerical value (integer) and populate the UI with the crime count.
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

Expand All @@ -51,8 +52,10 @@ This sample uses the [Crimes in Police Beats Sample](https://www.arcgis.com/home

## Additional information

This sample uses the `GeoView-Compose` module of the [ArcGIS Maps SDK for Kotlin Toolkit](https://developers.arcgis.com/kotlin/toolkit/) to implement a Composable MapView.

Visit [Getting Started](https://developers.arcgis.com/arcade/) on the *ArcGIS Developer* website to learn more about Arcade expressions.

## Tags

Arcade evaluator, Arcade expression, identify layers, portal, portal item, query
Arcade evaluator, Arcade expression, geoview-compose, identify layers, portal, portal item, query, toolkit
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
"keywords": [
"Arcade evaluator",
"Arcade expression",
"geoview-compose",
"identify layers",
"portal",
"portal item",
"query",
"toolkit",
"ArcadeEvaluationResult",
"ArcadeEvaluator",
"ArcadeExpression",
Expand All @@ -31,7 +33,9 @@
"PortalItem"
],
"snippets": [
"src/main/java/com/esri/arcgismaps/sample/queryfeatureswitharcadeexpression/MainActivity.kt"
"src/main/java/com/esri/arcgismaps/sample/queryfeatureswitharcadeexpression/components/QueryFeaturesWithArcadeExpressionViewModel.kt",
"src/main/java/com/esri/arcgismaps/sample/queryfeatureswitharcadeexpression/MainActivity.kt",
"src/main/java/com/esri/arcgismaps/sample/queryfeatureswitharcadeexpression/screens/QueryFeaturesWithArcadeExpressionScreen.kt"
],
"title": "Query features with arcade expression"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.arcgismaps.android.library)
alias(libs.plugins.arcgismaps.android.library.compose)
alias(libs.plugins.arcgismaps.kotlin.sample)
alias(libs.plugins.gradle.secrets)
}
Expand All @@ -11,9 +12,7 @@ secrets {

android {
namespace = "com.esri.arcgismaps.sample.queryfeatureswitharcadeexpression"
// For view based samples
buildFeatures {
dataBinding = true
buildConfig = true
}
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

<uses-permission android:name="android.permission.INTERNET" />

<application><activity
android:exported="true"
<application>
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/query_features_with_arcade_expression_app_name">

</activity>
</application>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/*
* Copyright 2023 Esri
/* 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.
Expand All @@ -17,193 +16,38 @@

package com.esri.arcgismaps.sample.queryfeatureswitharcadeexpression

import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.lifecycleScope
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import com.arcgismaps.ApiKey
import com.arcgismaps.ArcGISEnvironment
import com.arcgismaps.arcade.ArcadeEvaluator
import com.arcgismaps.arcade.ArcadeExpression
import com.arcgismaps.arcade.ArcadeProfile
import com.arcgismaps.data.ArcGISFeature
import com.arcgismaps.mapping.ArcGISMap
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.mapping.PortalItem
import com.esri.arcgismaps.sample.queryfeatureswitharcadeexpression.databinding.QueryFeaturesWithArcadeExpressionActivityMainBinding
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
import com.esri.arcgismaps.sample.queryfeatureswitharcadeexpression.screens.QueryFeaturesWithArcadeExpressionScreen

class MainActivity : AppCompatActivity() {

// set up data binding for the activity
private val activityMainBinding: QueryFeaturesWithArcadeExpressionActivityMainBinding by lazy {
DataBindingUtil.setContentView(this, R.layout.query_features_with_arcade_expression_activity_main)
}

private val mapView by lazy {
activityMainBinding.mapView
}

private val infoTextView by lazy {
activityMainBinding.infoTextView
}

// progress indicator
private val progressBar by lazy {
activityMainBinding.progressBar
}

// setup the red pin marker image as a bitmap drawable
private val markerDrawable: BitmapDrawable by lazy {
// load the bitmap from resources and create a drawable
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.map_pin_symbol)
BitmapDrawable(resources, bitmap)
}

// setup the red pin marker as a Graphic
private val markerGraphic: Graphic by lazy {
// creates a symbol from the marker drawable
val markerSymbol = PictureMarkerSymbol.createWithImage(markerDrawable).apply {
// resize the symbol into a smaller size
width = 30f
height = 30f
// offset in +y axis so the marker spawned is right on the touch point
offsetY = 25f
}
// create the graphic from the symbol
Graphic(symbol = markerSymbol)
}

// create a graphic overlay
private val graphicsOverlay = GraphicsOverlay()
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)
lifecycle.addObserver(mapView)

// create a portal item with the itemId of the web map
val portal = Portal("https://www.arcgis.com/")
val portalItem = PortalItem(portal, "539d93de54c7422f88f69bfac2aebf7d")
// create and add a map with with portal item
val map = ArcGISMap(portalItem)
// add the marker graphic to the graphics overlay
graphicsOverlay.graphics.add(markerGraphic)
mapView.apply {
this.map = map
// add the graphics overlay to the MapView
graphicsOverlays.add(graphicsOverlay)
}

lifecycleScope.launch {
// show an error and return if the map load failed
map.load().onFailure {
return@launch showError("Error loading map:${it.message}")
}

// get the RPD Beats layer from the map's operational layers
val policeBeatsLayer = map.operationalLayers.firstOrNull { layer ->
layer.id == "RPD_Reorg_9254"
} ?: return@launch showError("Error finding RPD Beats layer")

// capture and collect when the user taps on the screen
mapView.onSingleTapConfirmed.collect { event ->
// update the marker location to where the user tapped on the map
event.mapPoint?.let { point ->
markerGraphic.geometry = point
mapView.setViewpointCenter(point)
}
// evaluate an Arcade expression on the tapped screen coordinate
evaluateArcadeExpression(event.screenCoordinate, map, policeBeatsLayer)
setContent {
SampleAppTheme {
QueryFeaturesWithArcadeExpressionApp()
}
}
}

/**
* Evaluates an Arcade expression that returns crime in the last 60 days at the tapped
* [screenCoordinate] on the [map] with the [policeBeatsLayer] and displays the result
* in a textview
*/
private suspend fun evaluateArcadeExpression(
screenCoordinate: ScreenCoordinate,
map: ArcGISMap,
policeBeatsLayer: Layer
) {
// show the progress indicator as the Arcade evaluation can take time to complete
progressBar.visibility = View.VISIBLE
// identify the layer and its elements based on the position tapped on the mapView and
// get the result
val result = mapView.identifyLayer(
layer = policeBeatsLayer,
screenCoordinate = screenCoordinate,
tolerance = 12.0,
returnPopupsOnly = false
)
// get the result as an IdentifyLayerResult
val identifyLayerResult = result.getOrElse { error ->
// if the identifyLayer operation failed show an error and return
showError("Error identifying layer:${error.message}")
// reset the text view to show its default text
infoTextView.text = getString(R.string.tap_to_begin)
// dismiss the progress indicator
progressBar.visibility = View.GONE
return
}
// if there are no geoElements identified
if (identifyLayerResult.geoElements.isEmpty()) {
// since the layer is a feature layer, display that no features were found
infoTextView.text = getString(R.string.no_features_found)
// dismiss the progress indicator
progressBar.visibility = View.GONE
return
@Composable
private fun QueryFeaturesWithArcadeExpressionApp() {
Surface(color = MaterialTheme.colorScheme.background) {
QueryFeaturesWithArcadeExpressionScreen(
sampleName = getString(R.string.query_features_with_arcade_expression_app_name)
)
}
// 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 ArcadeExpression using the string expression
val arcadeExpression = ArcadeExpression(expressionValue)
// create an ArcadeEvaluator with the ArcadeExpression and an ArcadeProfile
val arcadeEvaluator = ArcadeEvaluator(arcadeExpression, ArcadeProfile.FormCalculation)
// create a map of profile variables with the feature and map as key value pairs
val profileVariables = mapOf<String, Any>("\$feature" to identifiedFeature, "\$map" to map)
// evaluate using the previously set profile variables and get the result
val evaluationResult = arcadeEvaluator.evaluate(profileVariables)
// get the result as an ArcadeEvaluationResult
val arcadeEvaluationResult = evaluationResult.getOrElse { error ->
// if the evaluation failed show an error and return
showError("Error evaluating Arcade expression:${error.message}")
// reset the text view to show its default text
infoTextView.text = getString(R.string.tap_to_begin)
// dismiss the progress indicator
progressBar.visibility = View.GONE
return
}
// get the crimes count from the arcadeEvaluationResult as a numerical double value
val crimesCount = arcadeEvaluationResult.result as Double
// display this result in a textview
infoTextView.text = getString(R.string.crime_info_text, crimesCount.toInt())
// hide the progress indicator
progressBar.visibility = View.GONE
}

private fun showError(message: String) {
Log.e(localClassName, message)
Snackbar.make(mapView, message, Snackbar.LENGTH_SHORT).show()
}
}
Loading
Loading