Skip to content

Commit 74be5b8

Browse files
authored
Adds "Style features with custom dictionary" sample (#431)
1 parent ae94e4a commit 74be5b8

File tree

10 files changed

+485
-0
lines changed

10 files changed

+485
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Style features with custom dictionary
2+
3+
Use a custom dictionary style created from a web style or local style file (.stylx) to symbolize features using a variety of attribute values.
4+
5+
![Image of style features with custom dictionary](style-features-with-custom-dictionary.png)
6+
7+
## Use case
8+
9+
When symbolizing geoelements in your map, you may need to convey several pieces of information with a single symbol. You could try to symbolize such data using a unique value renderer, but as the number of fields and values increases, that approach becomes impractical. With a dictionary renderer you can build each symbol on-the-fly, driven by one or more attribute values, and handle a nearly infinite number of unique combinations.
10+
11+
## How to use the sample
12+
13+
Use the radio buttons to toggle between the dictionary symbols from the web style and style file. Pan and zoom around the map to see the symbology from the chosen dictionary symbol style. The web style and style file are slightly different to each other to give a visual indication of the switch between the two.
14+
15+
## How it works
16+
17+
1. Create a `PortalItem`, referring to a `Portal` and the item ID of the web style.
18+
2. Based on the style selected:
19+
* If the web style toggle has been selected, create a new `DictionarySymbolStyle` from the portal item using `DictionarySymbolStyle(portalItem)`, and load it
20+
* If the file style toggle has been selected, create a new `DictionarySymbolStyle` using `DictionarySymbolStyle.createFromFile(stylxFile.getAbsolutePath())`
21+
3. Create a new `DictionaryRenderer`, providing the dictionary symbol style.
22+
4. Apply the dictionary renderer to a feature layer using `featureLayer.renderer = dictionaryRenderer`.
23+
5. Add the feature layer to the map's operational layers using `getOperationalLayers().add(featureLayer)`.
24+
25+
## Relevant API
26+
27+
* DictionaryRenderer
28+
* DictionarySymbolStyle
29+
* Portal
30+
* PortalItem
31+
32+
## About the data
33+
34+
The data used in this sample is from a feature layer showing a subset of [restaurants in Redlands, CA](https://www.arcgis.com/home/item.html?id=3daf83e1ec0941428526a07f2d2ae414) hosted as a feature service with attributes for rating, style, health score, and open hours.
35+
36+
The feature layer is symbolized using a dictionary renderer that displays a single symbol for all of these variables. The renderer uses symbols from a custom restaurant dictionary style created from a [stylx file](https://arcgis.com/home/item.html?id=751138a2e0844e06853522d54103222a) and a [web style](https://arcgis.com/home/item.html?id=adee951477014ec68d7cf0ea0579c800), available as items from ArcGIS Online, to show unique symbols based on several feature attributes. The symbols it contains were created using ArcGIS Pro. The logic used to apply the symbols comes from an Arcade script embedded in the stylx file (which is a SQLite database), along with a JSON string that defines expected attribute names and configuration properties.
37+
38+
## Additional information
39+
40+
For information about creating your own custom dictionary style, see the open source [dictionary renderer toolkit](https://esriurl.com/DictionaryToolkit) on *GitHub*.
41+
42+
## Tags
43+
44+
dictionary, military, portal, portal item, renderer, style, stylx, unique value, visualization
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"category": "Visualization",
3+
"description": "Use a custom dictionary style created from a web style or local style file (.stylx) to symbolize features using a variety of attribute values.",
4+
"formal_name": "StyleFeaturesWithCustomDictionary",
5+
"ignore": false,
6+
"images": [
7+
"style-features-with-custom-dictionary.png"
8+
],
9+
"keywords": [
10+
"dictionary",
11+
"military",
12+
"portal",
13+
"portal item",
14+
"renderer",
15+
"style",
16+
"stylx",
17+
"unique value",
18+
"visualization",
19+
"DictionaryRenderer",
20+
"DictionarySymbolStyle",
21+
"Portal",
22+
"PortalItem"
23+
],
24+
"language": "kotlin",
25+
"redirect_from": "",
26+
"relevant_apis": [
27+
"DictionaryRenderer",
28+
"DictionarySymbolStyle",
29+
"Portal",
30+
"PortalItem"
31+
],
32+
"snippets": [
33+
"src/main/java/com/esri/arcgismaps/sample/stylefeatureswithcustomdictionary/components/StyleFeaturesWithCustomDictionaryViewModel.kt",
34+
"src/main/java/com/esri/arcgismaps/sample/stylefeatureswithcustomdictionary/MainActivity.kt",
35+
"src/main/java/com/esri/arcgismaps/sample/stylefeatureswithcustomdictionary/DownloadActivity.kt",
36+
"src/main/java/com/esri/arcgismaps/sample/stylefeatureswithcustomdictionary/screens/StyleFeaturesWithCustomDictionaryScreen.kt"
37+
],
38+
"title": "Style features with custom dictionary"
39+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
plugins {
2+
alias(libs.plugins.arcgismaps.android.library)
3+
alias(libs.plugins.arcgismaps.android.library.compose)
4+
alias(libs.plugins.arcgismaps.kotlin.sample)
5+
alias(libs.plugins.gradle.secrets)
6+
}
7+
8+
secrets {
9+
// this file doesn't contain secrets, it just provides defaults which can be committed into git.
10+
defaultPropertiesFileName = "secrets.defaults.properties"
11+
}
12+
13+
android {
14+
namespace = "com.esri.arcgismaps.sample.stylefeatureswithcustomdictionary"
15+
buildFeatures {
16+
buildConfig = true
17+
}
18+
}
19+
20+
dependencies {
21+
// Only module specific dependencies needed here
22+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-permission android:name="android.permission.INTERNET" />
5+
6+
<application><activity
7+
android:exported="true"
8+
android:name=".MainActivity"
9+
android:label="@string/style_features_with_custom_dictionary_app_name">
10+
11+
</activity>
12+
<activity
13+
android:name=".DownloadActivity"
14+
android:exported="true"
15+
android:label="@string/style_features_with_custom_dictionary_app_name" />
16+
</application>
17+
18+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* Copyright 2025 Esri
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
package com.esri.arcgismaps.sample.stylefeatureswithcustomdictionary
17+
18+
import android.content.Intent
19+
import android.os.Bundle
20+
import com.esri.arcgismaps.sample.sampleslib.DownloaderActivity
21+
22+
class DownloadActivity : DownloaderActivity() {
23+
override fun onCreate(savedInstanceState: Bundle?) {
24+
super.onCreate(savedInstanceState)
25+
downloadAndStartSample(
26+
Intent(this, MainActivity::class.java),
27+
getString(R.string.style_features_with_custom_dictionary_app_name),
28+
listOf(
29+
"https://www.arcgis.com/home/item.html?id=751138a2e0844e06853522d54103222a",
30+
)
31+
)
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/* Copyright 2025 Esri
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
package com.esri.arcgismaps.sample.stylefeatureswithcustomdictionary
18+
19+
import android.os.Bundle
20+
import androidx.activity.ComponentActivity
21+
import androidx.activity.compose.setContent
22+
import androidx.compose.material3.MaterialTheme
23+
import androidx.compose.material3.Surface
24+
import androidx.compose.runtime.Composable
25+
import com.arcgismaps.ApiKey
26+
import com.arcgismaps.ArcGISEnvironment
27+
import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
28+
import com.esri.arcgismaps.sample.stylefeatureswithcustomdictionary.screens.StyleFeaturesWithCustomDictionaryScreen
29+
30+
class MainActivity : ComponentActivity() {
31+
32+
override fun onCreate(savedInstanceState: Bundle?) {
33+
super.onCreate(savedInstanceState)
34+
// authentication with an API key or named user is
35+
// required to access basemaps and other location services
36+
ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
37+
38+
setContent {
39+
SampleAppTheme {
40+
StyleFeaturesWithCustomDictionaryApp()
41+
}
42+
}
43+
}
44+
45+
@Composable
46+
private fun StyleFeaturesWithCustomDictionaryApp() {
47+
Surface(color = MaterialTheme.colorScheme.background) {
48+
StyleFeaturesWithCustomDictionaryScreen(
49+
sampleName = getString(R.string.style_features_with_custom_dictionary_app_name)
50+
)
51+
}
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/* Copyright 2025 Esri
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
package com.esri.arcgismaps.sample.stylefeatureswithcustomdictionary.components
18+
19+
import android.app.Application
20+
import androidx.lifecycle.AndroidViewModel
21+
import androidx.lifecycle.viewModelScope
22+
import com.arcgismaps.mapping.ArcGISMap
23+
import com.arcgismaps.mapping.BasemapStyle
24+
import com.arcgismaps.mapping.Viewpoint
25+
import com.arcgismaps.mapping.layers.FeatureLayer
26+
import com.arcgismaps.mapping.symbology.DictionaryRenderer
27+
import com.arcgismaps.mapping.symbology.DictionarySymbolStyle
28+
import com.arcgismaps.portal.Portal
29+
import com.arcgismaps.mapping.PortalItem
30+
import com.arcgismaps.data.ServiceFeatureTable
31+
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
32+
import com.esri.arcgismaps.sample.stylefeatureswithcustomdictionary.R
33+
import kotlinx.coroutines.flow.MutableStateFlow
34+
import kotlinx.coroutines.launch
35+
import java.io.File
36+
37+
private const val RESTAURANTS_SERVICE_URL =
38+
"https://services2.arcgis.com/ZQgQTuoyBrtmoGdP/arcgis/rest/services/Redlands_Restaurants/FeatureServer/0"
39+
private const val WEB_STYLE_ITEM_ID = "adee951477014ec68d7cf0ea0579c800"
40+
41+
class StyleFeaturesWithCustomDictionaryViewModel(app: Application) : AndroidViewModel(app) {
42+
43+
// Feature layer showing restaurant data in Redlands, CA
44+
private val restaurantFeatureLayer = FeatureLayer.createWithFeatureTable(
45+
featureTable = ServiceFeatureTable(uri = RESTAURANTS_SERVICE_URL)
46+
)
47+
var arcGISMap = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
48+
initialViewpoint = Viewpoint(
49+
latitude = 34.0543,
50+
longitude = -117.1963,
51+
scale = 1e4
52+
)
53+
}
54+
private var dictionaryRendererFromStyleFile: DictionaryRenderer? = null
55+
private var dictionaryRendererFromWebStyle: DictionaryRenderer? = null
56+
57+
private val _selectedStyle = MutableStateFlow(CustomDictionaryStyle.StyleFile)
58+
59+
val messageDialogVM = MessageDialogViewModel()
60+
private val provisionPath: String by lazy {
61+
app.getExternalFilesDir(null)?.path.toString() + File.separator + app.getString(R.string.style_features_with_custom_dictionary_app_name)
62+
}
63+
64+
init {
65+
viewModelScope.launch {
66+
// Prepare the dictionary renderer from the style file
67+
dictionaryRendererFromStyleFile = createDictionaryRendererFromStyleFile()
68+
// Prepare the dictionary renderer from the web style
69+
dictionaryRendererFromWebStyle = createDictionaryRendererFromWebStyle().getOrElse {
70+
messageDialogVM.showMessageDialog(it)
71+
return@launch
72+
}
73+
74+
// Apply the renderer for the initially selected style
75+
applyRendererForSelectedStyle()
76+
77+
// Add the restaurant layer to the map and load the map
78+
arcGISMap.apply {
79+
operationalLayers.add(restaurantFeatureLayer)
80+
}.load().onFailure {
81+
messageDialogVM.showMessageDialog(it)
82+
}
83+
}
84+
}
85+
86+
/**
87+
* Update the current dictionary style selection and apply the renderer to the feature layer.
88+
*/
89+
fun updateSelectedStyle(style: CustomDictionaryStyle) {
90+
_selectedStyle.value = style
91+
applyRendererForSelectedStyle()
92+
}
93+
94+
/**
95+
* Apply the dictionary renderer to the restaurants layer.
96+
*/
97+
private fun applyRendererForSelectedStyle() {
98+
restaurantFeatureLayer.renderer = when (_selectedStyle.value) {
99+
CustomDictionaryStyle.StyleFile -> dictionaryRendererFromStyleFile
100+
CustomDictionaryStyle.WebStyle -> dictionaryRendererFromWebStyle
101+
}
102+
}
103+
104+
/**
105+
* Create a dictionary renderer from the style file included in the app's assets.
106+
*/
107+
private fun createDictionaryRendererFromStyleFile(): DictionaryRenderer {
108+
val restaurantsStyleFile = File(provisionPath, "Restaurant.stylx")
109+
check(restaurantsStyleFile.exists()) {
110+
"Style file not found. Expected at: ${restaurantsStyleFile.canonicalPath}"
111+
}
112+
val restaurantStyle = DictionarySymbolStyle.createFromFile(restaurantsStyleFile.path)
113+
114+
return DictionaryRenderer(dictionarySymbolStyle = restaurantStyle)
115+
}
116+
117+
/**
118+
* Create a dictionary renderer from the web style hosted as a Portal item.
119+
* Maps the feature layer's field "healthgrade" to the dictionary style's expected field "Inspection".
120+
*/
121+
private suspend fun createDictionaryRendererFromWebStyle(): Result<DictionaryRenderer> {
122+
val portal = Portal(url = "https://www.arcgis.com")
123+
val portalItem = PortalItem(portal = portal, itemId = WEB_STYLE_ITEM_ID)
124+
val restaurantSymbolStyle = DictionarySymbolStyle(portalItem).apply {
125+
load().getOrElse {
126+
return Result.failure(it)
127+
}
128+
}
129+
130+
return Result.success(
131+
DictionaryRenderer(
132+
dictionarySymbolStyle = restaurantSymbolStyle,
133+
symbologyFieldOverrides = mapOf("healthgrade" to "Inspection")
134+
)
135+
)
136+
}
137+
}
138+
139+
/**
140+
* Enum representing the two custom dictionary styles available in this sample.
141+
*/
142+
enum class CustomDictionaryStyle {
143+
StyleFile,
144+
WebStyle
145+
}

0 commit comments

Comments
 (0)